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
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
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.