I simulate lazy evaluation, i.e. evaluate only when needed and only once, in JS with Proxy and run into a problem with Traversable (mapA instead of traverse at the term level):
const r = List.mapA({map: Opt.map, ap: Opt.ap, of: Opt.of})
(x => (x & 1) === 0 ? null : x)
(List.fromArr([1,3,5]));
Opt.map(Arr.fromList) (r); // yields [1,3,5]
Please note that Opt isn't an ADT but based on null. This works as expected: r is a thunk. Arr.fromList is strict and eventually forces evaluation of r. The problem arises when I trigger short circuiting, i.e. pass List.fromArr([1,2,3]) to List.mapA. When Opt.map forces the evaluation of the outermost List layer it yields the first elem 1 and the tail of the list, which is an unevaluated thunk again. Hence Opt.map assumes continuous computation and applies r to Arr.fromList, which in turn forces the evaluation of the entire List. 2 causes the short circuiting but not within Opt.map but within List.fromArr. Hence an error is thrown.
The problem is that due to lazy evaluation the transformation of r from List to null is deferred and thus not handled by the Opt functor anymore but by the pure functon invoked by it. How does a lazy language like Haskell tackle this issue? Or am I making a mistake in between?
Unfortunately, I cannot provide a running example because it would include a huge amount of library code. Here are implementations of the core functions, in case someone is interested:
List.mapA = ({map, ap, of}) => {
const liftA2_ = liftA2({map, ap}) (List.Cons);
return f => List.foldr(x => acc =>liftA2_(f(x)) (acc)) (of(List.Nil));
};
const liftA2 = ({map, ap}) => f => tx => ty => ap(map(f) (tx)) (ty);
List.foldr = f => acc => function go(tx) { // guarded rec if `f` non-strict in 2nd arg
return tx.run({
nil: acc,
cons: y => ty => f(y) (lazy(() => go(ty)))
// ^^^^^^^^^^^^^^^^^^ thunk
});
};
[EDIT]
Here is a more detailed description of the computation. A thunk is not just the usual nullary function () => .. but guarded by a Proxy, so that it can be used as a placeholder for any expression in most cases.
Constructing r
List.fromArrcreates a List{head: 1, tail}with the tail fully evaluatedList.mapAapplies the head1to itsf, which yields1and the overall op in turn returns{head: 1, tail: thunk}, i.e. the tail is a thunk (since1is odd there is no short circuiting)- The evaluation stops because
List.foldr, of whichList.mapAis derived of, is non-strict in the tail revaluated to{head: 1, tail: thunk}, i.e. an expression that contains a thunk and is now in weak head normal form (WHNF) (and isn't a thunk itself as I claimed in my original question)- please note that there is no
Optlayer because optional values are encoded usingnull, not as an ADT
Convert the traversed list back to Array again
- Lifting the strict
Arr.fromListwithOpt.mapis necessary becausermay benull Opt.mapinspects the outermost layer of the list in WHNFr, so no further evaluation takes place at this point since the expression is already in WHNF- it detects a
List, which is equivalent toJust [a], and invokes the pure functionArr.fromListto further processr Arr.fromListis strict, i.e. it recursively evaluates the tail until the base case is hit- during the second recursion it evaluates the tail, which is a thunk, to
{head: f(2), tail: thunk}, wherefis the one fromList.mapA f(2)evaluates tonulland short circuits the original list traversalArr.fromListexpects another list layer, notnulland throws an error
I have a hunch that List.mapA must be strict in its list argument. I could easily do it, because there is a strict version of foldr based on tail recursion modulo cons. However, I doesn't understand the underlying principle when to make an operation strict. If list traversals were strict, so would have to be monadic list chains. That would be a bummer.
Something is wrong here.
Opt.mapdoes not force the evaluation of theListlayer, all it does is to force the evaluation of theOptlayer and leave the rest to the mapping callback. (Or really,Opt.mapwould return a thunk for this operation, it wouldn't do this immediately).If you do
then
ralready isnull(or a thunk for it, if you want). Callingon this will never even call
Arr.fromList, it will just forwardnull.If this is not what is happening in your code, there either is a bug in your lazy thunk implementation or in your
Opt(map,ap,of) implementation.Re update:
No no no. This sounds like you are confusing
List.mapAwithList.map.mapAmust run through the entire list to apply the applicative effects before returning a result (though of course, it may return a thunk for this instead, but evaluating that to WHNF must distinguish betweenNothing(null) andJust, where the contents ofJustmay be lazy still).It doesn't matter that
foldRdoes not evaluate the tail itself but only passes a thunk tof, it is the responsibility ofOpt.apto look at that second argument to check whether the tail of the list evaluates toNothingor aJust.The layer is still there, even if
Justis not represented by a separate JS value. Notice that this might be the reason for causing all this trouble, since it makes it hard (impossible?) to distinguishthunk::Opt a(which may evaluate tonull::Opt a) from(Just thunk)::Opt a. Are you going to just makeOptstrict?Why should it do that? All that
Opt.mapneeds to do is check whether the outermost layer isnullor not. It shouldn't further evaluateJustcontents that are not in WHNF.Yes, but this is not done by choosing a strict
foldRvariant. It is done byliftA2/apforcing evaluation of the list tail.