Why does cons work in this context with lazy-seq, but conj doesn’t?
This works:
(defn compound-interest [p i]
(cons p (lazy-seq (compound-interest (* p (+ 1 i)) i))))
This doesn’t (it gives a stack overflow exception):
(defn compound-interest2 [p i]
(conj (lazy-seq (compound-interest2 (* p (+ 1 i)) i)) p))
(conj collection item)addsitemtocollection. To do that, it needs to realizecollection. (I’ll explain why below.) So the recursive call happens immediately, rather than being deferred.(cons item collection)creates a sequence which begins withitem, followed by everything incollection. Significantly, it doesn’t need to realizecollection. So the recursive call will be deferred (because of usinglazy-seq) until somebody tries to get the tail of the resulting sequence.I’ll explain how this works internally:
consactually returns aclojure.lang.Consobject, which is what lazy sequences are made of.conjreturns the same type of collection which you pass it (whether that is a list, vector, or whatever else).conjdoes this using a polymorphic Java method call on the collection itself. (See line 524 ofclojure/src/jvm/clojure/lang/RT.java.)What happens when that Java method call happens on the
clojure.lang.LazySeqobject which is returned bylazy-seq? (HowConsandLazySeqobjects work together to form lazy sequences will become clearer below.) Look at line 98 ofclojure/src/jvm/clojure/lang/LazySeq.java. Notice it calls a method calledseq. This is what realizes the value of theLazySeq(jump to line 55 for the details).So you could say that
conjneeds to know exactly what kind of collection you passed it, butconsdoesn’t.consjust requires that the “collection” argument is anISeq.Note that
Consobjects in Clojure are different from “cons cells” in other Lisps — in most Lisps, a “cons” is just an object which holds 2 pointers to other arbitrary objects. So you can use cons cells to build trees, and so on. A ClojureConstakes an arbitraryObjectas head, and anISeqas tail. SinceConsitself implementsISeq, you can build sequences out ofConsobjects, but they can just as well point to vectors, or lists, etc. (Note that a “list” in Clojure is a special type (PersistentList), and is not built fromConsobjects.)clojure.lang.LazySeqalso implementsISeq, so it can be used as the tail (“cdr” in Lisps) of aCons. ALazySeqholds a reference to some code which evaluates to anISeqof some kind, but it doesn’t actually evaluate that code until required, and after it does evaluate the code, it caches the returnedISeqand delegates to it.…is this all starting to make sense? Do you get the idea of how lazy sequences work? Basically, you start with a
LazySeq. When theLazySeqis realized, it evaluates to aCons, which points to anotherLazySeq. When that one is realized… you get the idea. So you get a chain ofLazySeqobjects, each holding (and delegating to) aCons.About the difference between “conses” and “lists” in Clojure, “lists” (
PersistentListobjects) contain a cached “length” field, so they can respond tocountin O(1) time. This wouldn’t work in other Lisps, because in most Lisps, “lists” are mutable. But in Clojure they are immutable, so caching the length works.Consobjects in Clojure don’t have a cached length — if they did, how could they be used to implement lazy (and even infinite) sequences? If you try to take thecountof aCons, it just callscounton its tail, and then increments the result by 1.