4

I've been trying to translate this recursive Haskell implementation of a futumorphism specialized to Lists

futuL :: (a -> Maybe (b, ([b], Maybe a))) -> a -> [b]
futuL f x = case f x of
  Nothing -> []
  Just (y, (ys, mz)) -> y : (ys ++ fz)
    where fz = case mz of
      Nothing -> []
      Just z -> futuL f z

into an imperative Javascript loop. It is remarkably difficult (at least for me):

const None = ({type: "Option", tag: "None"});
const Some = x => ({type: "Option", tag: "Some", runOption: x});

const arrFutu = coalg => x => { // futuL f x
  const acc = []; // ~ fz

  while (true) {
    let optX = coalg(x); // f x

    switch (optX.tag) { // case
      case "None": return acc; // Nothing -> []

      case "Some": {
        let [y, [ys, optX_]] = optX.runOption; // Just (y, (ys, mz))

        switch(optX_.tag) {
          case "None": {
            arrPushFlat(acc) ((ys.unshift(y), ys)); // y : (ys ++ [])
            return acc;
          }

          case "Some": { // y : (ys ++ futuL f z)
            arrPushFlat(acc) ((ys.unshift(y), ys)); 
            x = optX_.runOption;
            break;
          }

          default: throw new UnionError("invalid tag");
        }

        break;
      }

      default: throw new UnionError("invalid tag");
    }
  }
};

I have a difficult time to parse the Haskell code mentally, especially the where part containing the recursive call. I guess this call isn't in tail position, therefore I need an explicit stack (acc) for my JS loop.

Is my translation correct?

duplode
  • 33,731
  • 7
  • 79
  • 150

1 Answers1

2

Because Haskell is lazy, one can start consuming the beginning of the list returned by the "futu" before the rest has been calculated. In Javascript terms, this is best modeled with generators.

For example (for simplicity, I used null values to represent the Nones):

const arrFutu = coalg => function*(seed) {
    while (true) {
        const next = coalg(seed);
        if (next) {
            // Maybe (b, ([b], Maybe a)), using null for None & flattening nested tuple   
            const [item, items, nextseed] = next; 
            yield item;
            yield* items;
            // Maybe a, using null for None 
            if (nextseed) { 
                seed = nextseed;
                continue; // Continue iterating if the seed isn't null.
            }
        } 
        return;
    }
}

With an example coalgebra like:

const coalg1 = seed => {
    if (seed < 5) {
        return ['a', ['a','a'], seed + 1];
    } else if (seed < 10) {
        return ['b', ['b','b'], seed + 1];
    } else if (seed == 10) {
        return ['c', ['c'], null];
    }
    return null;
}

for (const x of arrFutu(coalg1)(0)) {
    console.log(x);
}

for (const x of arrFutu(coalg1)(20)) {
    console.log(x);
}

It would be nice to make the futu recursive instead of iterative, but it seems Javascript tail call optimization doesn't work with generators.


By the way, even if the Haskell code isn't tail recursive, the recursion is "guarded": the recursive call happens within one or more data constructors (here, list constructors) and the call can be delayed until the constructors have been "peeled away" by the client when he is consuming the returned list.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • Thank you for adding an example - great answer. I guess lazy evaluation is also essential to understand the dual of futu, histo. –  Jun 01 '19 at 16:19
  • 1
    Unfortunately, there is no TCO in Javascript even though it is part of the spec (no major browser vendor implemented it or will implement it any time soon. And yes, I know guarded recursion/TCO modulo cons and also implemented it in form of a trampoline in JS. –  Jun 01 '19 at 16:20
  • 2
    @bob Lazy evaluation fits futu better because futu is an unfold, an "anamorphism" that builds a value from a seed. The value that is built might even be infinite. Meanwhile, histos are "catamorphims" that tear down an existing finite value. Strict evaluation works well there. The particularity of histos wrt normal catamorphisms is that at each step they have access to a record of all previous sub-results. – danidiaz Jun 01 '19 at 16:36
  • That sound reasonable. I just figured that for the `List` type `histo` seems to be implemented in terms of `cata`, which is just `foldr` in this context, i.e. the lazy evaluated right associative fold. I need to do some research on this subject. For me personally, lazy evaluation is one of the most complicated aspects of Haskell. –  Jun 01 '19 at 16:57