[Python-ideas] Tail recursion elimination
Terry Reedy
tjreedy at udel.edu
Sun Jan 19 00:09:39 CET 2014
On 1/18/2014 9:52 AM,
musicdenotation at gmail.com wrote:
A few notes first:
1) A tail call is a 'top level' call in a return statement.
return f(*args, **kwds)
A directly recursive call, where f refers to the function with the
return statement, is a special case.
2) Whether a tail call is directly recursive cannot be determined, in
Python, at compile time merely by looking at the function definition. If
'f' is the definition name of the function, an assumption can be made,
but doing so is a semantic change in the language.
3). 'Tail recursion elimination' has two quite different meanings.
3a. Compile the (in Python, assumed to be) directly recursive function
as if it had been written with a while loop. This is typically done in
languages that do not expose while loops. This eliminates the function
call overhead as well as stack growth. This does nothing for indirect
recursion through intermediate function calls.
3b. At runtime, make the call but reuse the current stack frame. This is
easily done (at least in CPython) for all tail calls. But doing so for
all tail calls will make stack traces pretty useless, as tail calls are
rather common. Determining whether the call is recursive, to limit
stack reuse, takes extra work. Either choice only eliminates stack growth.
Comments on some of you responses to a *subset* of TRE issues.
> On April 22, 2009, Guido van Rossum wrote:
>> First, <the stack trace issue>
...
> Do [while/for] loops have nice stack traces as you mean it?
No, if you want a complete trace, you must add a print statement inside
the loop. Looping with tail recursion gives you the complete trace for free.
>> Second, <what is fundamental>
...
What is fundamental, besides alternation, is the ability to express an
unbounded amount of repetition, with variation, in a small finite amount
of code. I call this computational induction, in analogy to
mathematical induction. The alternative implemenations include recursion
and explicit while/for loops. There are also the combinator and fixed
point approaches.
>> <writing repetition with explicit loops>
> I think the most obvious way to do something is how it is /defined/.
Most recursive definitions are naturally written with body recursion
rather than tail recursion. A simple example (without input checking) is
the factorial function.
def fact(n):
if n: return fact(n-1) * n
else: return 1
To get TRE, one must re-write in tail-recursive form. (Python default
arguments actually make this easier than in most languages.)
def fact(N, n=1, result=1):
if n<N: return fact(n+1, result*(n+1)
else: return result
I have intentionally written the tail form so it calculates intermediate
results in the same order, making no assumption that the reduction
operator is either cummutative or associative. Once the work of
converting to tail form is done, conversion to while form is trivial and
easier than the conversion from body to tail recursion.
def fact(N):
n, result = 1, 1
while n < N:
n, result = n+1, result*(n+1)
But in fact, Python has another alternative, for loops.
def fact(N):
result = 1
for n in range(2, N):
result = result * n
return result
For loops are the proper way to write most collection processing that
one could write with tail recursion, as it makes use of Python's
iterator protocol.
def sum(iterable):
sum = 0
for n in iterable:
sum = sum + n
return sum
To write that with either recursion or while, one must call iter() and
next() and catch StopIteration oneself. Try it (I have) and see if you
really think it more natural. Lisp/scheme get away with recursion for
collection processing because there is only one collection type and
clever pattern-matching syntax to deconstruct an instance to get one
element at a time. I personally consider the efficiency of for loops,
for both programmer and computer, to be a major reason to not bother
with TRE.
--
Terry Jan Reedy
More information about the Python-ideas
mailing list