2

This question uses the following "lazy list" (aka "stream") type:

type 'a lazylist = Cons of 'a * (unit -> 'a lazylist)

My question is: how to define a tail-recursive function lcycle that takes a non-empty (and non-lazy) list l as argument, and returns the lazylist corresponding to repeatedly cycling over the elements l. For example:

# ltake (lcycle [1; 2; 3]) 10;;
- : int list = [1; 2; 3; 1; 2; 3; 1; 2; 3; 1]

(ltake is a lazy analogue of List::take; I give one implementation at the end of this post.)

I have implemented several non-tail-recursive versions of lcycles, such as:

let lcycle l =
  let rec inner l' =
    match l' with
    | []   -> raise (Invalid_argument "lcycle: empty list")
    | [h]  -> Cons (h, fun () -> inner l)
    | h::t -> Cons (h, fun () -> inner t)
  in inner l

...but I have not managed to write a tail-recursive one.

Basically, I'm running into the problem that lazy evaluation is implemented by constructs of the form

Cons (a, fun () -> <lazylist>)

This means that all my recursive calls happen within such a construct, which is incompatible with tail recursion.

Assuming the lazylist type as defined above, is it possible to define a tail-recursive lcycle? Or is this inherently impossible with OCaml?

EDIT: My motivation here is not to "fix" my implementation of lcycle by making it tail-recursive, but rather to find out whether it is even possible to implement a tail recursive version of lcycle, given the definition of lazylist above. Therefore, pointing out that my lcycle is fine misses what I'm trying to get at. I'm sorry I did not make this point sufficiently clear in my original post.


This implementation of ltake, as well as the definition of the lazylist type above, comes from here:

let rec ltake (Cons (h, tf)) n =
  match n with
    0 -> []
  | _ -> h :: ltake (tf ()) (n - 1)
kjo
  • 33,683
  • 52
  • 148
  • 265
  • Will creation of a lazy_list ever involve the concept of being tail-recursive or not? basically your code is already "tail-recursive" if you insist. Basically, creation of a lazy_list will never stack overflow, but usage on a lazy_list can. Your `ltake` is not tail-recursive and you can make it tail-recursive.http://typeocaml.com/2014/11/09/magic-of-thunk-stream-list/ – Jackson Tale Dec 12 '14 at 13:53

2 Answers2

1

I don't see much of a problem with this definition. The call to inner is within a function which won't be invoked until lcycle has returned. Thus there is no stack safety issue.

Here's an alternative which moves the empty list test out of the lazy loop:

let lcycle = function
  | [] -> invalid_arg "lcycle: empty"
  | x::xs ->
    let rec first = Cons (x, fun () -> inner xs)
    and inner = function
      | [] -> first
      | y::ys -> Cons (y, fun () -> inner ys) in
    first
gsg
  • 9,167
  • 1
  • 21
  • 23
1

The problem is that you're trying to solve a problem that doesn't exist. of_list function will not take any stack space, and this is why lazy lists are so great. Let me try to explain the process. When you apply of_list function to a non empty list, it creates a Cons of the head of the list and a closure, that captures a reference to the tail of the list. Afterwards it momentary returns. Nothing more. So it takes only few words of memory, and none of them uses stack. One word contains x value, another contains a closure, that captures only a pointer to the xs.

So then, you deconstruct this pair, you got the value x that you can use right now, and function next, that is indeed the closure that, when invoked, will be applied to a list and if it is nonempty, will return another Cons. Note, that previous cons will be already destroyed to junk, so new memory won't be used.

If you do not believe, you can construct an of_list function that will never terminate (i.e., will cycle over the list), and print it with a iter function. It will run for ever, without taking any memory.

type 'a lazylist = Cons of 'a * (unit -> 'a lazylist)

let of_list lst =
  let rec loop = function
    | [] -> loop lst
    | x :: xs -> Cons (x, fun () -> loop xs) in
  loop lst

let rec iter (Cons (a, next)) f =
  f a;
  iter (next ()) f
ivg
  • 34,431
  • 2
  • 35
  • 63