It’s a great question, and isn’t immediately obvious from reading just the documentation.
lazy-seq
is a macro which creates a LazySeq
instance. The reason it is a macro is that whatever you pass to it, it wraps that up in an 0-arity function (aka thunk), which is a way of creating a delayed computation. A lazy sequence is really a delay, with a slightly different mechanism to triggering its evaluation than an actual delay.
With a delay, you can pass in any body of expressions, and it is only evaluated when you dereference the delay, via either deref
or @
.
With a lazy sequence, you can only pass in a body which evaluates to a seq, that is anything for which seqable?
returns true. When you call lazy-seq
at the REPL, provided you pass it a valid body, it will be realized immediately. However, if you call it and bind its value to a symbol (via def
or within a let
block or similar), then it won’t do anything with the body you pass it, until you actual request it, for example by calling take
, nth
and so on, on it.
Now, here’s the real kicker: your function isn’t actually using recursion! It just looks that way. When you call my-repeat
as follows:
(def result1 (my-repeat 5))
your function returns immediately with the result of the following:
(cons 5 (lazy-seq (my-repeat 5))
which actually creates a Cons instance, which is seqable, and which has two items: 5 and a lazy sequence. It is important to note that the Cons instance (which when you print it in the REPL looks like a list, delimited using parentheses) is not lazy.
If you just call say:
(nth result1 0)
and don’t attempt to evaluate the whole return value, say by printing it to the REPL, then the lazy sequence which is at index 1 is never evaluated, and you just retrieve the item at index 0, which is 5.
If you then call
(nth result1 1)
now the lazy sequence which is the item at index 1 of result1
above is evaluated for the very first time. So up until now result1
has looked like:
(cons 5 <<unevaluated lazy sequence>> )
but now, when we try to evaluate the lazy sequence, it becomes:
(cons 5 (cons 5 <<unevaluated lazy sequence>> ))
which can be expressed as:
(5 5 <<unevaluated lazy sequence>> )
since it is a Cons instance which looks and behaves like a list in most ways, at least when it comes to retrieving items from it.
The above is now the new state of result1
after our call to (nth result1 1)
. If this sounds like result1
has been mutated, that’s because it has. Or specifically, the lazy sequence that was initially at index 1, after our original call to my-repeat
, has now been realized, and will always return the value of 5 whenever we attempt to call say (nth result1 1)
.
At this stage result1
appears as if it has two realized items followed by a lazy sequence (at least from the point of view of any function we use to request items from it is concerned.) If we attempt to request an index which has already been realized, we get back the item at that index. If we attempt to retrieve an item which has not yet been realized, what happens is that the last lazy sequence is evaluated, then its value is returned, and that will contain the next item in the sequence together with another lazy sequence, and so on, until we return the item at the index we requested.
So each time we realize a lazy sequence at the end of result1
, we return the next element together with a new lazy sequence. We only ever do this outside the body of my-repeat
, hence recursion is not even possible.