Others have already answered the general question. Let me add something on this specific point:
Is there something special about lists that causes this, or is the
idea more general than that just lists?
No, lists are not special. Every data
type in Haskell has a lazy semantics. Let's try a simple example using the pair type for integers (Int, Int)
.
let pair :: (Int,Int)
pair = (1, fst pair)
in snd pair
Above, fst,snd
are the pair projections, returning the first/second component of a pair. Also note that pair
is a recursively defined pair. Yes, in Haskell you can recursively define everything, not just functions.
Under a lazy semantics, the above expression is roughly evaluated like this:
snd pair
= -- definition of pair
snd (1, fst pair)
= -- application of snd
fst pair
= -- definition of pair
fst (1, fst pair)
= -- application of fst
1
By comparison, using an eager semantics, we would evaluate it like this:
snd pair
= -- definition of pair
snd (1, fst pair)
= -- must evaluate arguments before application, expand pair again
snd (1, fst (1, fst pair))
= -- must evaluate arguments
snd (1, fst (1, fst (1, fst pair)))
= -- must evaluate arguments
...
In the eager evaluation, we insist on evaluating arguments before applying fst/snd
, and we obtain a infinitely looping program. In some languages this will trigger a "stack overflow" error.
In the lazy evaluation, we apply functions soon, even if the argument is not fully evaluated. This makes snd (1, infiniteLoop)
return 1
immediately.
So, lazy evaluation is not specific to lists. Anything is lazy in Haskell: trees, functions, tuples, records, user-defined data
types, etc.
(Nitpick: if the programmer really asks for them, it is possible to define types having strict / eagerly-evaluated components. This can be done using strictness annotations, or using extensions such as unboxed types. While sometimes these have their uses, they're are not commonly found in Haskell programs.)