In this case, a picture may tell a thousand words.
First, remember how cons (the (:)
list constructor) works. It's a pair of two things: an element, and a reference to a list tail (which is either another cons, or []
).
As you should know, when you say [1, 2, 3]
, it's just a shortcut for (1:(2:(3:[])))
or 1:2:3:[]
. If you visualize each cons pair as a box with two slots, this expression look like:
┌───┬──┐ ┌───┬──┐ ┌───┬──┐ ┌────┐
│ 1 │ ─┼─>│ 2 │ ─┼─>│ 3 │ ─┼─>│ [] │
└───┴──┘ └───┴──┘ └───┴──┘ └────┘
One loop
When you say g = 4 : g
, you're not really building an "infinite" list, you're building a circular list: g
is defined as a cons whose tail reference simply points back to g
itself:
┌──────────┐
│ ┌───┬──┐ │
└>│ 4 │ ─┼─┘
└───┴──┘
This actually has nothing to do with laziness, and everything with self-reference: for example, you can do the same thing in (eager) Common Lisp using syntax like '#1=(4 . #1#)
(where the #1
is like g
).
Whether you say g !! 0
, or g !! 1000000000000
, g
never grows: (!!)
simply runs around the loop in-place, for as many times as you told it to, until it exhausts itself and returns the element, 4
.
Two loops
When you say f = (f !! 10) : f
, the same thing happens—except now, the element slot contains a different expression than 4
:
┌──────────┐
│ ┌───┬──┐ │
└>│ ╷ │ ─┼─┘
└─┼─┴──┘
│
│ ┌───────────┐
└>│ (f !! 10) │
└───────────┘
Crucially, this sub-expression also refers back to f
, just like the tail does:
┌──────────┐
│ ┌───┬──┐ │
┌┴>│ ╷ │ ─┼─┘
│ └─┼─┴──┘
│ │
│ │ ┌───────────┐
│ └>│ (f !! 10) │
│ └──┼────────┘
└─────────┘
So, when you ask for f !! n
, (!!)
will first run around the top loop n
times, then return the element, as it did for g
. However, instead of escaping the loop, (f !! 10)
just re-enters it, and the process repeats itself: looping 10 times around the top, then once around the bottom, and back.