Well, let's illustrate it. First, here's a simple definition of doubleMe
:
doubleMe [] = []
doubleMe (x:xs) = x*2 : doubleMe xs
Now, let's consider how a strict/eager language (not Haskell) evaluates doubleMe (doubleMe (doubleMe [1, 2, 3]))
. The rule in such a language is this: to evaluate a function call, evaluate all of the arguments completely, then pass them to the function. So we get this:
doubleMe (doubleMe (doubleMe [1, 2, 3]))
-- Expand the list literal into its structure
== doubleMe (doubleMe (doubleMe (1 : 2 : 3 : [])))
-- Eager evaluation requires that we start at the innermost use of doubleMe
-- and work there until we produce the whole list.
== doubleMe (doubleMe (2 : doubleMe (2 : 3 : [])))
== doubleMe (doubleMe (2 : 4 : doubleMe (3 : [])))
== doubleMe (doubleMe (2 : 4 : 6 : doubleMe []))
== doubleMe (doubleMe (2 : 4 : 6 : []))
-- Only now we can move on to the middle use of doubleMe:
== doubleMe (4 : doubleMe (4 : 6 : []))
== doubleMe (4 : 8 : doubleMe (6 : []))
== doubleMe (4 : 8 : 12 : doubleMe [])
== doubleMe (4 : 8 : 12 : [])
== 8 : doubleMe (8 : 12 : [])
== 8 : 16 : doubleMe (12 : [])
== 8 : 16 : 24 : doubleMe []
== 8 : 16 : 24 : []
== [8, 16, 24]
In Haskell, the rule is more like this (but not exactly so):
- To evaluate a function application, you apply the function before you evaluate its arguments.
- If the outer function has multiple cases (like our
doubleMe
definition does), then we evaluate only enough of its argument to figure out which case to use.
So we get something like:
doubleMe (doubleMe (doubleMe [1, 2, 3]))
-- Here we only "pull out" the 1 from the list, because it's all we need to
-- pick which case we want for doubleMe.
== doubleMe (doubleMe (doubleMe (1 : [2, 3])))
== doubleMe (doubleMe (1*2 : doubleMe [2, 3]))
-- Now instead of continuing with the inner doubleMe, we move on immediately
-- to the middle one:
== doubleMe ((1*2)*2 : doubleMe (doubleMe [2, 3]))
-- And now, since we know which case to use for the outer doubleMe, we expand
-- that one:
== (1*2)*2)*2 : doubleMe (doubleMe (doubleMe [2, 3]))
And in Haskell, evaluation stops here unless there is another caller that demands the value of the head or tail of the list. (Note that I didn't even do the multiplications.) For example, head
is the function that returns the first element of a list:
head (x:xs) = x
Suppose we were evaluating head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
. This is how it would go:
head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
-- repeat the steps from above for the doubleMe part
head (((1*2)*2)*2 : doubleMe (doubleMe (doubleMe [2, 3]))
-- By the definition of head:
((1*2)*2)*2
So the doubleMe (doubleMe (doubleMe [2, 3]))
part is discarded in this case, because head
doesn't need it to produce a result. If Haskell wasn't lazy, it'd compute the whole [8, 12, 24]
list and then take the 8
off the front.
GHC is even smarter than this. We can use the map
function to write doubleMe
:
doubleMe = map (*2)
GHC, when you use the -O
option to optimize the compiled program, has this rule baked into it:
map f (map g xs) = map (f . g) xs
This means that if it sees nested uses of map
, it can reduce them to one pass over the list. Using that:
head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
== head (map (*2) (map (*2) (map (*2) [1, 2, 3])))
== head (map ((*2) . (*2) . (*2)) [1, 2, 3])
== head (map (\x -> ((x*2)*2)*2) [1, 2, 3])
== head (map (\x -> ((x*2)*2)*2) (1 : [2, 3]))
== head (((1*2)*2)*2 : map (\x -> ((x*2)*2)*2) [2, 3])
== ((1*2)*2)*2
EDIT: There's clearly a lot of confusion on the topic of one pass vs. three passes in the answers to this question. I'll throw my lot in.
Simple lazy evaluation (as shown in my second example evaluation) doesn't change the number of passes. If we evaluate something like print (doubleMe (doubleMe (doubleMe [1, 2, 3])))
, lazy and eager evaluation will do the same amount of work, but in different orders. Let's write the values of the nested expressions and line up the list elements, like this:
doubleMe [1, 2, 3] = [2, 4, 6]
doubleMe (doubleMe [1, 2, 3]) = [4, 8, 12]
doubleMe (doubleMe (doubleMe [1, 2, 3])) = [8, 16, 24]
Now, if we do something like print (doubleMe (doubleMe (doubleMe [1, 2, 3])))
:
- Eager evaluation will attack this row-by-row. First it will compute the whole list
[2, 4, 6]
, then the list [4, 8, 12]
, then the list [8, 16, 24]
, and finally it will print the last list.
- Lazy evaluation attacks this column-by-column. First it will compute the
2
in the first list, then the 4
in the second list, then the 8
in the first list, and print the 8
; then compute the 4
in the first list, the 8
in the second list, and so on.
If you're printing the list, since that requires computing all of the elements, it's (more or less) the same amount of work and memory in either case. In the head (doubleMe (doubleMe (doubleMe [1, 2, 3])))
example, however, lazy evaluation does less work.
And the final, "fusion" example (the one using map f . map g == map (f . g)
) does less work than the two others, because it's truly one pass. But that's not because of laziness, that's because purity allows for more aggressive optimization.