In Haskell, you can build infinite lists due to laziness:
Prelude> let g = 4 : g
Prelude> g !! 0
4
Prelude> take 10 g
[4,4,4,4,4,4,4,4,4,4]
"Doesn't exist yet" is not quite right. We try not to think about when values are exist — denotational programming is about eternal unchanging values and equations.
More concretely, this code is fine:
Prelude> let x = [(x !! 1) + 1, 3] in x
[4,3]
You might expect that x !! 1
doesn't exist yet. But here's how an implementation like GHC works.
When building the list f
, it constructs an in-memory object (a "thunk") to represent the expression (x !! 1) + 1
. No evaluation of x
has occurred yet. It wraps a pointer to this thunk, plus a pointer to 3
, into a linked list and hands it off to GHCi's implicit show
.
Now, the show
instance for lists has to show the elements one by one. show
will demand ("force") the evaluation of the thunk for (x !! 1) + 1
, which causes that code to be "entered". By the definition of (+)
, forcing (x !! 1) + 1
forces x !! 1
, which in turn forces two things:
(+)
wants to operate on that value.And the second value is present by now — it's 3
. If it were another thunk we'd force that, and so on. See this blog post for an interesting view on self-referential containers.
Now, how does the compiled GHC code detect an infinite loop in your other example? When we enter a thunk, we need to remember to come back later and overwrite it with the final value. This is what's meant specifically by "lazy evaluation" as opposed to "non-strict semantics", and it prevents us from duplicating work.
Anyway, as an optimization when entering a thunk, GHC's runtime will first overwrite it with another object called a "blackhole". We'll come back later and overwrite the blackhole with the final value. But what happens if we enter a blackhole before doing so? That means that evaluating x
requires first evaluating x
, an unresolvable loop. So entering a blackhole throws an exception.