I don’t mean "Why didn’t the authors of Batteries make
LazyList.map2 lazy, but rather “What makes its code non-lazy?”
The doc comment for
LazyList.map2 in Batteries says:
Raises Different_list_size if the two lists have different lengths. Not tail-recursive, lazy. In particular, the exception is raised only after the shortest list has been entirely consumed.
I am interpreting the second sentence as “Not tail-recursive and not lazy” (as opposed to “Not tail-recursive, but lazy”).
One question is about the phrase “In particular”. How does the fact that one must consume one of the lists fully to get the exception make the function non-lazy? (Or non-tail-recursive?? No.)
Looking at the source, though, I’m not clear on what is and isn’t lazy. The definition is:
let map2 f l1 l2 = let rec aux l1 l2 = match (next l1, next l2) with | (Cons (h1, t1), Cons(h2, t2)) -> lazy (Cons (f h1 h2, aux t1 t2)) | (Nil, Nil) -> nil | (Cons _, Nil) | (Nil, Cons _) -> raise (Different_list_size "LazyList.map2") in aux l1 l2
Compare this with
LazyList.map, whose doc comment says it is a “Lazy map”:
let map f l = let rec aux rest = match next rest with | Cons (x, (t : 'a t)) -> Cons (f x, lazy (aux t)) | Nil -> Nil in lazy (aux l)
They differ in that in
lazy is applied to the
Cons, while in
lazy is inside the
Cons, and applies to computation of the next node. So there is something lazy about
map2, but it’s different from
map's laziness. I don’t understand the difference, though.
It seems as if
map2 is even more lazy than
map, you immediately apply
f to the head, and the rest of the list is lazily constructed. In
f isn’t even applied to the head immediately.