I believe the idea must be that in a lazy language if you're evaluating a function application, it must be because you need the result of the application for something. So whatever reason caused the function application to be reduced in the first place is going to continue to need to reduce the returned result. If we didn't need the function's result we wouldn't be evaluating the call in the first place, the whole application would be left as a thunk.
A key point is that the standard "lazy evaluation" order is demand-driven. You only evaluate what you need. Evaluating more risks violating the language spec's definition of "non-strict semantics" and looping or failing for some programs that should be able to terminate; lazy evaluation has the interesting property that if any evaluation order can cause a particular program to terminate, so can lazy evaluation.1
But if we only evaluate what we need, what does "need" mean? Generally it means either
- a pattern match needs to know what constructor a particular value is (e.g. I can't know what branch to take in your definition of
foo
without knowing whether the argument is []
or _:xs
)
- a primitive operation needs to know the entire value (e.g. the arithmetic circuits in the CPU can't add or compare thunks; I need to fully evaluate two
Int
values to call such operations)
- the outer driver that executes the
main
IO action needs to know what the next thing to execute is
So say we've got this program:
foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
main :: IO ()
main = print (head (foo [1..]))
To execute main
, the IO driver has to evaluate the thunk print (head (foo [1..]))
to work out that it's print
applied to the thunk head (foo [1..])
. print
needs to evaluate its argument on order to print it, so now we need to evaluate that thunk.
head
starts by pattern matching its argument, so now we need to evaluate foo [1..]
, but only to WHNF - just enough to tell whether the outermost list constructor is []
or :
.
foo
starts by pattern matching on its argument. So we need to evaluate [1..]
, also only to WHNF. That's basically 1 : [2..]
, which is enough to see which branch to take in foo
.2
The :
case of foo
(with xs
bound to the thunk [2..]
) evaluates to the thunk map (*2) [2..]
.
So foo
is evaluated, and didn't evaluate its body. However, we only did that because head
was pattern matching to see if we had []
or x : _
. We still don't know that, so we must immediately continue to evaluate the result of foo
.
This is what the article means when it says functions are strict in their result. Given that a call to foo
is evaluated at all, its result will also be evaluated (and so, anything needed to evaluate the result will also be evaluated).
But how far it needs to be evaluated depends on the calling context. head
is only pattern matching on the result of foo
, so it only needs a result to WHNF. We can get an infinite list to WHNF (we already did so, with 1 : [2..]
), so we don't necessarily get in an infinite loop when evaluating a call to foo
. But if head
were some sort of primitive operation implemented outside of Haskell that needed to be passed a completely evaluated list, then we'd be evaluating foo [1..]
completely, and thus would never finish in order to come back to head
.
So, just to complete my example, we're evaluating map (2 *) [2..]
.
map
pattern matches its second argument, so we need to evaluate [2..]
as far as 2 : [3..]
. That's enough for map
to return the thunk (2 *) 2 : map (2 *) [3..]
, which is in WHNF. And so it's done, we can finally return to head
.
head ((2 *) 2 : map (2 *) [3..])
doesn't need to inspect either side of the :
, it just needs to know that there is one so it can return the left side. So it just returns the unevaluated thunk (2 *) 2
.
Again though, we only evaluated the call to head
this far because print
needed to know what its result is, so although head
doesn't evaluate its result, its result is always evaluated whenever the call to head
is.
(2 *) 2
evaluates to 4
, print
converts that into the string "4"
(via show
), and the line gets printed to the output. That was the entire main
IO action, so the program is done.
1 Implementations of Haskell, such as GHC, do not always use "standard lazy evaluation", and the language spec does not require it. If the compiler can prove that something will always be needed, or cannot loop/error, then it's safe to evaluate it even when lazy evaluation wouldn't (yet) do so. This can often be faster so GHC optimizations do actually do this.
2 I'm skipping over a few details here, like that print
does have some non-primitive implementation we could step inside and lazily evaluate, and that [1..]
could be further expanded to the functions that actually implement that syntax.