Well, your code is building and passing along a "linked list" of closures (each of the closures captures the previous one as k
) in the heap instead of a stack of frames on the call stack.
A more common, equivalent way to do it tail-recursively is to pass along the result list so far (in reverse, since you can only efficiently add to the front), and then reverse it in the end:
let map f l =
let rec aux l acc = match l with
[] -> List.rev acc
| a::l -> aux l (f a :: l)
in aux l
(this is basically the same as List.rev (List.rev_map f l)
)
In this case, the thing that is accumulated is the list of results so far (in reverse), rather than a closure. But the effect is exactly the same.
In both cases we need linear space for some kind of intermediate representation (other than the input nor the output list), so in terms of complexity of memory usage, there is no advantage over the non-tail-recursive version. Although it is true that there is more memory on the heap than the stack, so using the tail-recursive version will probably work for bigger lists than the non-tail-recursive version. In terms of absolute memory usage, the accumulating the list option is probably the most efficient, because closures and stack frames both have more overhead.
map
function is a case of tail recursion modulo cons. – Premed