1

In attempting to understand lazy-seq, I came up with this example:

(defn zeroes []
  (list 0 (lazy-seq (zeroes))))

(take 5 (zeroes)) ; too much recursion error

This however triggers a too much recursion error. Replacing (list) with (cons) fixes the problem, but I don't understand why:

(defn zeroes []
  (cons 0 (lazy-seq (zeroes))))

(take 5 (zeroes))  ; returns (0 0 0 0 0)

My understanding of lazy-seq is that it immediately returns a lazy-seq instance but that its body is not evaluated until a call to first or rest on that instance. So I would think (zeroes) would just return a simple Cons of 0 and a LazySeq with a yet unevaluated body.

As an additional curiosity, I'm puzzled why this hangs the repl (because the repl attempts to print an infinite sequence) but doesn't trigger a 'too much recursion' error.

(defn zeroes []
  (cons 0 (lazy-seq (zeroes))))

(zeroes)  ; hangs the repl

(In case it's relevant, I'm trying these examples in the ClojureScript repl at http://himera.herokuapp.com/index.html.)

Jegschemesch
  • 11,414
  • 4
  • 32
  • 37

1 Answers1

1

You asked two questions...

1) Why does using list instead of cons in the following code result in infinite recursion?

(defn zeroes []
  (cons 0 (lazy-seq (zeroes))))

(take 5 (zeroes)) ; too much recursion error

The version using cons produces an infinite sequence of zeros, like this:

(0 0 0 0 ...)

If you use list instead, you produce a totally different result. You get an infinite nesting of lists of two elements each (with head=0 and tail=another list):

'(0 (0 (0 (0 (...))))

Since the top-level list only has two elements, you end up with the whole thing when you call (take 5). You get the "too much recursion" error when the REPL tries to print out these infinitely-nested lists.

Note that you could safely substitute the list* for cons. The list* function takes a variable number of arguments (as does list), but unlike list it assumes the last argument is a seq. This means (list* a b c d) is essentially just shorthand for (cons a (cons b (cons c d))). Since (list* a b) is basically the same as (cons a b), it follows that you can make the substitution in your code. In this case it probably wouldn't make much sense, but it's nice if you're cons-ing several items at the same time.


2) Why does the following diverge (hang) rather than throw a "too much recursion" error like we saw above?

(defn zeroes []
  (cons 0 (lazy-seq (zeroes))))

(zeroes)  ; hangs the repl

The zeros function produces a "flat" sequence of zeros (unlike the nested lists above). The REPL probably uses a tail-recursive function to evaluate each successive lazy element of the sequence. The tail-call optimization allows recursive functions to recur forever without blowing the call stack—so that's what happens. Recursive tail-calls in Clojure are denoted by the recur special form.

DaoWen
  • 32,589
  • 6
  • 74
  • 101
  • Thanks. I actually did figure out I was misunderstanding the cons/list distinction and what lazy-seq's body should return. I'm curious though why list* is so named when it returns a Cons rather than a PersistentList. Is it possible for the node chain of a PersistentList to end in a sequence? Or can only Cons do that? – Jegschemesch Aug 03 '14 at 01:05
  • The name `list*` is just tradition. I think all the other Lisp/Scheme variants have a similar function. [Common Lisp has it](http://www.lispworks.com/documentation/HyperSpec/Body/f_list_.htm#listST), and [so does Racket](http://docs.racket-lang.org/reference/pairs.html?q=list*#%28def._%28%28quote._~23~25kernel%29._list%2A%29%29). – DaoWen Aug 03 '14 at 03:54