The way of thinking about this that I read somewhere that really helped me on this front is to look at it this way:
- Every expression produces a thunk.
- Thunks will be forced when their value is "needed."
- The notion of "need" is what you may call "conditional forcing": "Assuming that thunk T is being forced, which other thunks will this cause to be forced?" A function is strict in its argument if forcing a thunk that applies that function causes its argument's thunk to be forced.
So, values are printed to the console by invoking the appropriate show
method on them; i.e., printing to the console forces an expression of form show x
(for some x
). Forcing show x
forces x
. Suppose x
is negate $ 5 * sqrt 16
; since negate
is strict in its argument, forcing the thunk also forces the thunk for 5 * sqrt 16
. Likewise, *
and sqrt
are both strict in their arguments, so the thunks for 5
, sqrt 16
and 16
must also be forced.
The other thing to understand is how data constructors and pattern matching affect thunking. Basically, unless there are special strictness anotations, constructors are like non-strict functions, in that forcing the thunk doesn't force the constructor's arguments. Unless the special lazy pattern matching syntax is used, matching against a constructor forces the argument thunk. So we have:
identity x = x -- irrefutable pattern; `x` is not forced
uncons (x:xs) = (x, xs) -- match against (:) constructor; argument
-- must be forced, but x and xs aren't forced
foo (x:x':xs) = (x, x', xs) -- two matches against (:) constructor;
-- the argument thunk is forced, as is its
-- tail thunk.