Search code examples
recursionclojurelazy-sequences

Does using `lazy-seq` recursively take up stack frames?


I'm learning about lazy seqs at the moment, and I've noticed that they usually involve recursion without using recur. For example, here is the implementation of iterate:

(defn iterate
  "Returns a lazy sequence of x, (f x), (f (f x)) etc. f must be free of side-effects"
  {:added "1.0"
   :static true}
  [f x] (cons x (lazy-seq (iterate f (f x)))))

I was under the impression that if you recurse without using recur, it will make a new stack frame for every call, which can cause stack overflows if you iterate enough times.

Does lazy-seq gobble up stack frames? If not, how does it avoid that?


Solution

  • lazy-seq is not actually recursing when you see it include a call to iterate in this example. Rather when you request the second item the lazy-seq macro unrolls itself into a suspended call. So each time you retrieve an item you are causing one more invocation, but the old call is not on the stack.

    Also of interest is whether you are consuming memory, and that depends on what you do with the head of the sequence. If you retain in a var or local binding, the entire chain of cons cells will be held. If you avoid holding the head, and traverse down the sequence, GC can clean up behind you.

    One tricky bit is that the lazy-seq macro has some special meta in it ^{:once true} that lets the compiler know that the fn closure is a single-time thing and not to hold onto any closed over variables.