Whilst starting to learn lisp, I\'ve come across the term tail-recursive. What does it mean exactly?
I'm not a Lisp programmer, but I think this will help.
Basically it's a style of programming such that the recursive call is the last thing you do.
An important point is that tail recursion is essentially equivalent to looping. It's not just a matter of compiler optimization, but a fundamental fact about expressiveness. This goes both ways: you can take any loop of the form
while(E) { S }; return Q
where E
and Q
are expressions and S
is a sequence of statements, and turn it into a tail recursive function
f() = if E then { S; return f() } else { return Q }
Of course, E
, S
, and Q
have to be defined to compute some interesting value over some variables. For example, the looping function
sum(n) {
int i = 1, k = 0;
while( i <= n ) {
k += i;
++i;
}
return k;
}
is equivalent to the tail-recursive function(s)
sum_aux(n,i,k) {
if( i <= n ) {
return sum_aux(n,i+1,k+i);
} else {
return k;
}
}
sum(n) {
return sum_aux(n,1,0);
}
(This "wrapping" of the tail-recursive function with a function with fewer parameters is a common functional idiom.)
In traditional recursion, the typical model is that you perform your recursive calls first, and then you take the return value of the recursive call and calculate the result. In this manner, you don't get the result of your calculation until you have returned from every recursive call.
In tail recursion, you perform your calculations first, and then you execute the recursive call, passing the results of your current step to the next recursive step. This results in the last statement being in the form of (return (recursive-function params))
. Basically, the return value of any given recursive step is the same as the return value of the next recursive call.
The consequence of this is that once you are ready to perform your next recursive step, you don't need the current stack frame any more. This allows for some optimization. In fact, with an appropriately written compiler, you should never have a stack overflow snicker with a tail recursive call. Simply reuse the current stack frame for the next recursive step. I'm pretty sure Lisp does this.
It means that rather than needing to push the instruction pointer on the stack, you can simply jump to the top of a recursive function and continue execution. This allows for functions to recurse indefinitely without overflowing the stack.
I wrote a blog post on the subject, which has graphical examples of what the stack frames look like.
The best way for me to understand tail call recursion
is a special case of recursion where the last call(or the tail call) is the function itself.
Comparing the examples provided in Python:
def recsum(x):
if x == 1:
return x
else:
return x + recsum(x - 1)
^RECURSION
def tailrecsum(x, running_total=0):
if x == 0:
return running_total
else:
return tailrecsum(x - 1, running_total + x)
^TAIL RECURSION
As you can see in the general recursive version, the final call in the code block is x + recsum(x - 1)
. So after calling the recsum
method, there is another operation which is x + ..
.
However, in the tail recursive version, the final call(or the tail call) in the code block is tailrecsum(x - 1, running_total + x)
which means the last call is made to the method itself and no operation after that.
This point is important because tail recursion as seen here is not making the memory grow because when the underlying VM sees a function calling itself in a tail position (the last expression to be evaluated in a function), it eliminates the current stack frame, which is known as Tail Call Optimization(TCO).
NB. Do bear in mind that the example above is written in Python whose runtime does not support TCO. This is just an example to explain the point. TCO is supported in languages like Scheme, Haskell etc
In short, a tail recursion has the recursive call as the last statement in the function so that it doesn't have to wait for the recursive call.
So this is a tail recursion i.e. N(x - 1, p * x) is the last statement in the function where the compiler is clever to figure out that it can be optimised to a for-loop (factorial). The second parameter p carries the intermediate product value.
function N(x, p) {
return x == 1 ? p : N(x - 1, p * x);
}
This is the non-tail-recursive way of writing the above factorial function (although some C++ compilers may be able to optimise it anyway).
function N(x) {
return x == 1 ? 1 : x * N(x - 1);
}
but this is not:
function F(x) {
if (x == 1) return 0;
if (x == 2) return 1;
return F(x - 1) + F(x - 2);
}
I did write a long post titled "Understanding Tail Recursion – Visual Studio C++ – Assembly View"