[Haskell-cafe] Re: Definition of "tail recursive" wrt Folds
Bertram Felgenhauer
bertram.felgenhauer at googlemail.com
Sat Mar 28 13:52:33 EDT 2009
Ben Franksen wrote:
> Mark Spezzano wrote:
> > Just looking at the definitions for foldr and foldl I see that foldl is
> > (apparently) tail recursive while foldr is not.
> >
> > Why?
> >
> > Is it because foldl defers calling itself until last whereas foldr
> > evaluates itself as it runs?
> >
> > What, strictly speaking, is the definition of ”tail recursive” as opposed
> > to just “recursive”?
>
> An application of some function f inside another function g is in 'tail
> position' (or a 'tail call') if the result of applying f is the result of
> g. Operationally speaking, calling f is the very last thing g does. Tail
> calls can be optimized by a compiler (or interpreter) so that the call does
> not use additional stack; that is, the call can be replaced by a jump.
>
> A function is called ”tail recursive” (as opposed to just “recursive”) if
> the recursive call to itself is in tail position. If the compiler performs
> tail call optimization, tail recursive functions can work with constant
> stack space, similar to a (imperative) loop.
>
> Looking at a definition of foldl, e.g.
>
> foldl f z0 xs0 = lgo z0 xs0 where
> lgo z [] = z
> lgo z (x:xs) = lgo (f z x) xs
>
> you see that lgo calls itself in tail position, thus is tail recursive. In
> contrast, foldr can be defined as
>
> foldr k z xs = go xs where
> go [] = z
> go (y:ys) = y `k` go ys
>
> where you see that the result of go is not the recursive call to go.
> Instead, the result is y `k` go ys . Thus, foldr is not tail recursive.
>
> So, if you are saying that "foldl defers calling itself until last whereas
> foldr evaluates itself as it runs" then you got the right idea, I think.
> The point is that foldr still needs to do something (namely to apply (y
> `k`)) to the result of applying itself. It needs to remember to do so, and
> thus the stack grows linearly with the size of the list.
Sorry, but that's wrong. It would be right in a strict language. In Haskell,
the 'go ys' term is not evaluated straight away; it is instead turned into
a suspended evaluation (a "thunk") that is typically stored on the heap.
The following discussion is implementation specific, with ghc in mind.
Haskell itself has no notion of a stack.
In fact both using foldl and using foldr can produce stack overflows:
Prelude> foldl (+) 0 [1..10^7]
*** Exception: stack overflow
Prelude> foldr (+) 0 [1..10^7]
*** Exception: stack overflow
Let's examine why. First, consider the foldl case. For simplicity, I'll
ignore the evaluation of [1..10^7]. The first few evaluation steps are
foldl (+) 0 [1,2,3..1000000]
-> lgo 0 [1,2,3..1000000]
-> lgo (0+1) [2,3,4..1000000]
-> lgo ((0+1)+2) [3,4,5..100000]
None of these steps uses the stack - foldl is indeed tail recursive. However,
the ((0+1)+2) is a thunk on the heap. Continuing,
-> lgo ((...(0+1)+...)+9999999) [10000000]
-> lgo ((...(0+1)+...)+10000000) []
-> ((...(0+1)+...)+10000000)
Now we have to evaluate that huge thunk. This turns out to cause trouble
because + (for Integers) is strict. So in order to find x+10000000, the
code needs the value of x first. And that's where the stack gets
involved: the information of the pending addition, (?+10000000) is pushed
onto the stack, and evaluation proceeds with the first term.
Denoting the stack by [[item1, item2, ...]], evaluation continues like
this:
-> [[(?+10000000)]] ((...(0+1)+...)+9999999)
-> [[(?+10000000),(?+9999999)]]
The stack will keep growing, until the (0+1) is reached, or the stack
overflows.
To make things more confusing, ghc has a strictness analyzer that
sometimes manages to avoid such a thunk being built up. For an
example how strictness helps see foldl' (below).
Now for the foldr case. Evaluation in that case looks a bit different:
foldr (+) 0 [1,2,3..10000000]
-> go [1,2,3..10000000]
-> 1 + go [2,3,4..10000000]
No stack was used so far; the go [2,3,4..1000000] is a thunk. Now, as
above, we need to add two numbers. And that's where the stack gets
involved again:
-> [[(1+?)]] go [2,3,4..10000000]
-> [[(1+?)]] 2 + go [3,4,5..10000000]
-> [[(1+?),(2+?)]] go [3,4,5..10000000]
-> [[(1+?),(2+?),(3+?)]] go [4,5,6..10000000]
and so on, until we reach go [] or the stack overflows. Note that
there is no reference to 'go' on the stack at all.
If instead of (+), we had a lazy function like (:), the stack would
not get involved in this way:
foldr (:) [] [1,2,3..10000000]
-> go [1,2,3..10000000]
-> 1 : go [2,3,4..10000000]
Which is in weak head normal form, so evaluation stops here. Later,
when other code examines the list, the 'go [2,3,4..10000000]' thunk
will get evaluated.
As a final note, the stack overflow with foldl above is cured by using
foldl', which is _strict_ in the accumulator.
For reference,
foldl' f z0 xs0 = lgo z0 xs0
where lgo z [] = z
lgo z (x:xs) = let z' = f z x in z' `seq` lgo z' xs
foldl' (+) 0 [1,2,3..1000000]
-> lgo 0 [1,2,3..1000000]
-> let z' = 0+1 in z' `seq` lgo z' [2,3,4..1000000]
Now the `seq` forces z' to be evaluated:
-> let z' = 1 in lgo z' [2,3,4..10000000]
-> lgo 1 [2,3,4..10000000]
-> let z' = 1+2 in z' `seq` lgo z' [3,4,5..10000000]
-> lgo 3 [3,4,5..10000000]
-> lgo 6 [4,5,6..10000000]
...
-> lgo 50000005000000 []
-> 50000005000000
HTH,
Bertram
More information about the Haskell-Cafe
mailing list