Pragmatics

Efficiency concerns:

tex2html_wrap_inline134 Time

tex2html_wrap_inline134 Space (= Time!)

Reasonable assumptions for tracking resource use:

tex2html_wrap_inline134 Applying a constructor allocates heap space.

code42

tex2html_wrap_inline134 Evaluating a fn expression allocates heap space for free variables.

code46

tex2html_wrap_inline134 Applying a function takes time.

Only recursive applications really matter. But not all recursive applications are equally expensive!

Costs of Recursion

Consider this version of the sum function:

code50

Or, equivalently,

code52

What's involved in evaluating this function on a large list?

At each recursive call sum t, must:
tex2html_wrap_inline134 store the value of local variable h
tex2html_wrap_inline134 store the current pc
tex2html_wrap_inline134 pass the argument t
tex2html_wrap_inline134 jump to the beginning of sum

On return, must:
tex2html_wrap_inline134 pass back the result value r
tex2html_wrap_inline134 retrieve the old pc and jump there
tex2html_wrap_inline134 restore the value of h
Only then can the addition h+r be performed.

Tail Recursion

Compare this version of sum:

code63

Notice that the recursive call to sum' is the last thing that happens in the function body. Such a call is said to be a tail call, and sum' is said to be tail-recursive.

In performing the tail-recursive call to sum', the same steps could be performed as before, but this is not necessary.

tex2html_wrap_inline134 Because the caller does nothing after the return (except return itself), there is really no need for the recursive call to return control to the caller.

tex2html_wrap_inline134 Instead, the callee can return its value directly to the top of the recursion (the call from sum).

tex2html_wrap_inline134 So there's no need for the recursive call to save any local variables nor to store the current pc.

In short, a tail-recursive call can be performed just by passing the argument values and then performing a simple jump to the beginning of the called function.

Tail Recursion and Iteration

This is in every way much cheaper than an ordinary call. Indeed, it's no more expensive than jumping to the top of an iterative loop in an imperative language.

In fact, it's essentially the same thing:

code74

Accumulation Parameters

The difference between sum and sum' is that the latter uses an extra parameter to accumulate the result, which is then returned by the nil case.

Adding an accumulation parameter is a common technique for changing non-tail-recursive functions to tail-recursive ones.

Contrast:

code81

The latter also avoids doing expensive append operations.

Reduce vs. Accumulate

Recall the general-purpose right-to-left reduce function

code85

which computes

(x tex2html_wrap_inline164 op (x tex2html_wrap_inline166 op ( tex2html_wrap_inline168 (x op a) ...)))

Note that it is not tail-recursive. But the similar left-to-right accumulate function is:

code91

which computes

(( tex2html_wrap_inline168 ((a op x tex2html_wrap_inline164 ) op x tex2html_wrap_inline166 ) tex2html_wrap_inline168 ) op x)

We can always rewrite a reduce as a (perhaps more complicated) accumulate. In the special case where op is associative and commutative, reduce and accumulate calculate the same thing.

Auxiliary Parameters

In general, it is more work to get rid of non-tail recursion in operations over trees. Consider

code100

We can use an accumulator to get rid of one non-tail call (and the append operation) by passing the accumlated flattening of all right-neighbors:

code102

Note that there is still one non-recursive call. We can get rid of it as well, by adding another extra parameter that keeps an explicit stack of left-neighbor trees to be flattened:

code105

Continuation functions

It turns out that we can always rewrite arbitrary recursive algorithms as tail-recursive ones by using explicit stack arguments like this. This is true in an ordinary imperative language too. (It's not clear that doing so will be a ``win,'' however, unless we can also make other improvements-such as getting rid of appends-in the process.)

There's another, remarkable, method by which we can turn a functional program into one that is completely tail-recursive using extra arguments of function type.

Consider sum again:

code50

And this version:

code112

Here sum' takes an extra argument k, called a continuation, which says ``what to do with the result.'' Although tail-recursive, this program isn't really any more efficient than the original one. (Any ideas why not?)


Andrew P. Tolmach
Thu May 22 18:29:48 PDT 1997