Documente Academic
Documente Profesional
Documente Cultură
Therefore, an important trick of the trade is knowing how to translate recursive algorithms into iterative algorithms. That way, you can design,
prove, and initially code your algorithms in the almighty realm of recursion.
Then, after youve got things just the way you want them, you can translate
your algorithms into equivalent iterative forms through a series of mechanical steps. You can prove your cake and run it in Python, too.
This topic turning recursion into iteration is fascinating enough that
Im going to do a series of posts on it. Tail calls, trampolines, continuationpassing style and more. Lots of good stuff.
For today, though, lets just look at one simple method and one supporting
trick.
Example: factorial
With a function this simple, we could probably go straight to the iterative
version without using any techniques, just a little noggin power. But the
point here is to develop a mechanical process that we can trust when our
functions arent so simple or our noggins arent so powered. So were going
to work on a really simple function so that we can focus on the process.
Ready? Then lets show these guys how cyber-commandos get it done! Mark
IV style!
1. Study the original function.
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)
Nothing scary here. Just one recursive call. We can do this!
2. Convert recursive calls into tail calls.
def factorial1a(n, acc=1):
if n < 2:
return 1 * acc
return factorial1a(n - 1, acc * n)
(If this step seemed confusing, see the Bonus Explainer at the end of
the article for the secret feature trick behind the step.)
3. Introduce a one-shot loop around the function body. You want
while True: body ; break .
def factorial1b(n, acc=1):
while True:
if n < 2:
return 1 * acc
stopped at step two with factorial1a . But nooooooo We had to press on,
all the way through step five, because were using Python. Its almost
enough to make a cyber-commando punch a kitten.
No, the work wasnt hard, but it was still work. So what did it buy us?
To see what it bought us, lets look inside the Python run-time environment. Well use the Online Python Tutors visualizer to observe the buildup of stack frames as factorial , factorial1a , and factorial1d each compute the factorial of 5.
This is very cool, so dont miss the link: Visualize It! (ProTip: Open it in a
new tab.)
Click the Forward button to step through the execution of the functions.
Notice what happens in the Frames column. When factorial is computing
the factorial of 5, five frames build up on the stack. Not a coincidence.
Same thing for our tail-recursive factorial1a . (Youre right. It is tragic.)
But not for our iterative wonder factorial1d . It uses just one stack frame,
over and over, until its done. Thats economy!
Thats why we did the work. Economy. We converted O(n) stack use into O(1)
stack use. When n could be large, that savings matters. It could be the difference between getting an answer and getting a segfault.
Not-so-simple cases
Okay, so we tackled factorial . But that was an easy one. What if your function isnt so easy? Then its time for more advanced methods.
Thats our topic for next time.
Until then, keep your brain recursive and your Python code iterative.
This conversion is easy once you get the hang of it, but the first few times
you see it, it seems like magic. So lets take this one step by step.
First, the problem. We want to get rid of the n * in the following code:
return n * factorial(n - 1)
That n * stands between our recursive call to factorial and the return
keyword. In other words, the code is actually equivalent to the following:
x = factorial(n - 1)
result = n * x
return result
That is, our code has to call the factorial function, await its result ( x ), and
then do something with that result (multiply it by n ) before it can return its
result. Its that pesky intermediate doing something we must get rid of. We
want nothing but the recursive call to factorial in the return statement.
So how do we get rid of that multiplication?
Heres the trick. We extend our function with a multiplication feature and use
all.
Second, I changed every single return statement from return {whatever} to
return acc * {whatever} . Whenever our function would have returned x , it
now returns acc * x . And thats it. Our secret feature is done! And its trivial to prove correct. (In fact, we just did prove it correct! Re-read the second
sentence.)
Both changes were entirely mechanical, hard to screw up, and, together,
default to doing nothing. These are the properties you want when adding
secret features to your functions.
Okay. Now we have a function that computes the factorial of n and, secretly, multiplies it by acc .
Now lets return to that troublesome line, but in our newly extended func-
tion:
return acc * n * factorial(n - 1)
It computes the factorial of n - 1 and then multiplies it by acc * n . But
wait! We dont need to do that multiplication ourselves. Not anymore. Now
we can ask our extended factorial function to do it for us, using the secret feature.
So we can rewrite the troublesome line as
return factorial(n - 1, acc * n)
And thats a tail call!
So our tail-call version of the factorial function is this:
def factorial(n, acc=1):
if n < 2:
return acc * 1
return factorial(n - 1, acc * n)
And now that all our recursive calls are tail calls there was only the one
this function is easy to convert into iterative form using The Simple Method
described in the main article.
Lets review the Secret Feature trick for making recursive calls into tail
calls. By the numbers:
1. Find a recursive call thats not a tail call.
2. Identify what work is being done between that call and its return
statement.
3. Extend the function with a secret feature to do that work, as controlled
by a new accumulator argument with a default value that causes it to do
nothing.
4. Use the secret feature to eliminate the old work.
5. Youve now got a tail call!
This task can be tricky. So we also discussed the Secret Feature trick for
putting recursive calls into tail-call form. That trick works well for simple
recursive calls, but when your calls arent so simple, you need a beefier version of the trick.
Thats the subject of this post: the Time-Traveling Secret Feature trick. Its
like sending a T-800 back in time to terminate a functions recursiveness
before it can ever offer resistance in the present.
Yeah.
But well have to work up to it. So stick with the early examples to prepare
for the cybernetic brain augmentations to come.
Enough talk! Lets start with a practical example.
Update 2013-06-17: If you want another example, heres also a step-by-step
conversion of the Fibonacci function.
to choose k items from a set of n items. For this reason, its often pro-
n
n!
( k ) = k!(n k)! ,
but that form causes all sorts of problems for computers. Fortunately, Concrete Mathematics helpfully points out a lifesaving absorption identity:
n
n n1
=
,
(k)
(
)
k k1
and we know what that is, dont we? Thats a recursive function just waiting
to happen!
And that identity, along with the base case ( 0 )
n
ning example:
n
x
k
x
k
equation does not necessarily hold because of finite precision or, in our
case, because were dealing with integer division that throws away remainders. (By the way, in Python, // is the division operator that throws away
remainders.) For this reason, I have been careful to use the form
n * binomial(n - 1, k - 1) // k
instead of the more literal translation
(n // k) * binomial(n - 1, k - 1)
which, if you try it, youll see often produces the wrong answer.
Okay, our challenge is set before us. Ready to de-recursivify binomial ?
For functions this simple, you can just hold them in your head and inline them into your code as needed. But for more-complicated functions, it really does help to break them out like this. For this example,
Im going to pretend that our work function is more complicated, just
to show how to do it.
3. Extend the function with a secret feature to do that work, as controlled by a
new accumulator argument in this case a pair ( lmul , rdiv ) with a
default value that causes it to do nothing.
def work(x, lmul, rdiv):
return lmul * x // rdiv
def binomial(n, k, lmul=1, rdiv=1):
if k == 0:
return work(1, lmul, rdiv)
return work(n * binomial(n - 1, k - 1) // k, lmul, rdiv)
Note that I just mechanically converted all return {whatever} statements into return work({whatever}, lmul, rdiv) .
4. Use the secret feature to eliminate the old work.
Watch what happens to that final line.
def work(x, lmul, rdiv):
return lmul * x // rdiv
def binomial(n, k, lmul=1, rdiv=1):
if k == 0:
return work(1, lmul, rdiv)
return binomial(n - 1, k - 1, lmul * n, k * rdiv)
5. Youve now got a tail call!
Indeed, we do! All thats left is to inline the sole remaining work call:
def binomial(n, k, lmul=1, rdiv=1):
if k == 0:
return lmul * 1 // rdiv
return binomial(n - 1, k - 1, lmul * n, k * rdiv)
first need to see it. So, once again, lets use the Online Python Tutors excellent Python-runtime visualizer. Open the link below in a new tab if you can,
and then read on.
Visualize the execution of binomial(39, 9) .
Click the Forward button to advance through each step of the computation.
When you get to step 22, where the recursive version has fully loaded the
stack with its nested frames, click slowly. Watch the return value (in red)
carefully as you advance. See how it gently climbs to the final answer of
211915132, never exceeding that value?
Now continue stepping through to the iterative version. Watch the value of
lmul as you advance. See how it grows rapidly, finally reaching a whopping
76899763100160?
This difference matters. While both versions computed the correct answer,
the original recursive version would do so without exceeding the capacity of
32-bit signed integers.1 The iterative version, however, needs a comparatively hoggish 47 bits to faithfully arrive at the correct answer.
Pythons integers, lucky for us, grow as needed to hold their values, so we
need not fear overflow in this case, but not all languages for which we
might want to use our recursion-to-iteration techniques offer such protections. Its something to keep in mind the next time youre taking the recursion out of an algorithm in C:
typedef int32_t Int;
Int binomial(Int n, Int k) {
if (k == 0)
return 1;
return n * binomial(n - 1, k - 1) / k;
}
Int binomial_iter(Int n, Int k) {
Int lmul = 1, rdiv = 1;
while (k > 0) {
lmul *= n; rdiv *= k; n -= 1; k -= 1;
}
return lmul / rdiv;
}
int main(int argc, char* argv[]) {
printf("binomial(39, 9)
= %ld\n", (long) binomial(39, 9));
printf("binomial_iter(39, 9) = %ld\n", (long) binomial_iter(39, 9));
}
/* Output with Int = int32_t:
binomial(39, 9)
= 211915132
binomial_iter(39, 9) = -4481
<-- oops!
*/
In any case, bigger integers are slower and consume more memory. In one
important way, the original, recursive algorithm was better. We have lost
something important.
Now lets get it back.
n(
until we hit the base case of k
n1
/k
)
k1
so:
5
4
3
=
5
/2
=
5
4
(2)
(1)
( ( 0 )/1) /2 = 5 (4 (1)/1)/2 = 10.
At every step (except for the base case) we perform a multiplication by n
and then a division by k . Its that division by k that keeps intermediate results from getting out of hand.
Now lets look at the iterative version. Its not so obvious whats going on.
(Score another point for recursion over iteration!) But we can puzzle it out.
We start out with both accumulators at 1 and then loop over the decreasing
values of k , building up the accumulators until k
5
lmul
((1) 5) 4
( 2 ) = rdiv = ((1) 2) 1 = 10.
Both the numerator and denominator grow and grow and grow until the final division. Not a good trend.
So why did the Secret Feature trick work great for factorial in our previous
article but fail us, albeit subtly, now? The answer is that in factorial the
extra work being done between each recursive call and its return statement
was multiplication and nothing more. And multiplication (of integers that
dont overflow) is commutative and associative. Meaning, ordering and
grouping dont matter.
The lesson, then, is this: Think carefully about whether ordering and
grouping matter before using the Secret Feature trick. If it matters, you
have two options: One, you can modify your algorithm so that ordering and
grouping dont matter and then use the Secret Feature trick. (But this option
is often intractable.) Two, you can use the Time-Traveling Secret Feature trick,
which preserves ordering and grouping. And that trick is what weve been
waiting for.
Its time.
some earlier answer xt1 that the function doesnt know, so it calls itself to
get that answer. And so on. Until, finally, it finds one concrete answer x0
that it actually knows and, from which, it can build every subsequent answer.
So, in truth, our answer xt is just the final element in a whole timeline of
needed earlier answers:
x0 , x1 , xt .
Well, so what? Why should we care?
Because weve watched The Terminator about six hundred times! We know
how to get rid of a problem in the present when weve seen its timeline: We
send a Terminator into the past to rewrite that timeline so the problem never gets
created in the rst place.
And our little recursive problem with binomial here? Weve seen its timeline.
Thats right. Its about to get real.
One more thing. Every single one of these steps preserves the original functions
behavior. You can run your unit tests after every step, and they should all
pass.
Lets begin.
1. Send a T-800 terminator unit into the functions timeline, back to
the time of x0 .
This might seem like overkill but prevents mistakes. Do you make mistakes? Then just make the helper function already. Im calling it step
for reasons that will shortly become obvious.
def step(n, k):
return n * binomial(n - 1, k - 1) // k
def binomial(n, k):
if k == 0:
return 1
return step(n, k)
3. Partition the helper function into its 3 fundamental parts.
They are:
# part (1)
# part (2)
# part (3)
secret location where the T-800 will drop values in the past and where
we will check for them in the future.
So, per prior arrangement with the T-800, well extend our helper
function with a secret feature that checks the dead drop for a previous
value xi1 and, if ones there, uses it to muahahahaha! break the recursive call chain:
def step(n, k, previous_x=None):
if previous_x is None:
previous_x = binomial(n - 1, k - 1)
x = n * previous_x // k
return x
(ni , ki , xi1 ) xi
Note that xi1 goes in and xi comes out. And from that little kernel of
power we can do some seriously crazy stuff. For example: reverse the
backward flow of time. That is, compute forward through the recursive
timeline instead of backward.
Yeah.
Read on.
5. Modify the helpers return statement to also pass through its non-
secret arguments.
def step(n, k, previous_x=None):
if previous_x is None:
previous_x = binomial(n - 1, k - 1)
x = n * previous_x // k
return (n, k, x) # <-- here
def binomial(n, k):
if k == 0:
return 1
return step(n, k)[2]
for xt , the function had to go back in the timeline to get xt1 . And to get
xt1 , it had to go back even further to get xt2 . And so on, chewing up
stack every backward step of the way to x0 . It was heartbreaking,
watching it work like that.
But now, we can step the other way. If we have any (ni , ki , xi1 ) we can
x0 , x1 , xt
ward.
So lets get those values!
7. Determine the initial conditions at the start of the timeline.
For this simple example, most of you can probably determine the initial
conditions by inspection. But Im going to go through the process anyway. You never know when you might need it. So:
Whats the start of the timeline? Its when the recursive binomial
function calls itself so many times that it finally hits one of its base
cases, which defines the first entry in the timeline, anchoring the
timeline at time i
since weve already split out the step logic; all thats left in binomial is
the base-case logic. Its easy to see that there is only one base case, and
its when k
= 0:
ni :
ki :
xi :
0 1 2 t
n
0 k
1 ?
(ni , ki , xi1 ) (ni+1 , ki+1 , xi ). So look at its logic: How does it transform its n and k arguments? It adds one to them. Thus,
ni+1 = ni + 1,
ki+1 = ki + 1.
Therefore,
k1 = k0 + 1 = 0 + 1 = 1.
Since ki is i steps from k0
ki = k0 + i(+1) = i.
And when i
n1 = nt (t 1)(+1) = n k + 1.
And now we have our initial conditions:
(n1 , k1 , x0 ) = (n k + 1, 1, 0).
So now our knowledge of the timeline looks like this:
time i :
ni :
ki :
xi :
0
1
2 t=k
nk nk+1
n
0
1
k
1
And with this knowledge, we can step forward through the timeline,
from time i
# <- new
(n, k, previous_x) = (n - k + 1, 1, 1)
for _i in xrange(1, t + 1):
(n, k, previous_x) = step(n, k, previous_x)
return previous_x # = x_t
#
#
#
#
n
= ( nk
) . One property of our code is that it runs for t = k steps. So when k > n k , we
n
can reduce the number of steps by solving for ( nk ) instead. Lets add
n
Thanks!
Well, thats it for this installment. I hope you enjoyed reading it as much as
I did writing it. If you liked it (or didnt), or if you found a mistake, or especially if you can think of any way to help make my explanations better, let
me know. Just post a comment or fire me a tweet at @tmoertel.
Until next time, keep your brain recursive and your Python code iterative.
1. In the visualization, you cant actually see the largest integer produced
by the recursive versions computation. Its produced between steps 32
and 33, when the return value from step 32 is multiplied by step 33s
The challenge
First, lets define a binary tree to be either empty or given by a node having
three parts: (1) a value, (2) a left subtree, and (3) a right subtree, where both
of the subtrees are themselves binary trees. In Haskell, we might define it
like so:
data BinaryTree a = Empty | Node a (BinaryTree a) (BinaryTree a)
In Python, which well use for the rest of this article, well say that None
represents an empty tree and that the following class represents a node:
import collections
Node = collections.namedtuple('Node', 'val left right')
# some sample trees having various node counts
tree0 = None # empty tree
tree1 = Node(5, None, None)
tree2 = Node(7, tree1, None)
tree3 = Node(7, tree1, Node(9, None, None))
tree4 = Node(2, None, tree3)
tree5 = Node(2, Node(1, None, None), tree3)
Let us now define a function to flatten a tree using an in-order traversal.
The recursive definition is absurdly simple, the data type having only two
cases to consider:
def flatten(bst):
# empty case
if bst is None:
return []
# node case
return flatten(bst.left) + [bst.val] + flatten(bst.right)
A few tests to check that it does what we expect:
def check_flattener(f):
assert f(tree0) == []
assert f(tree1) == [5]
assert f(tree2) == [5,
assert f(tree3) == [5,
assert f(tree4) == [2,
assert f(tree5) == [1,
print 'ok'
check_flattener(flatten)
7]
7, 9]
5, 7, 9]
2, 5, 7, 9]
# ok
Our challenge for today is to convert flatten into an iterative version. Oth-
er than a new trick partial evaluation the transformation is straightforward, so Ill move quickly.
Lets do this!
return left
def flatten(bst):
if bst is None:
return []
return step(bst)
And now well make step return values that parallel its input arguments:
def step(bst, left=None):
if left is None:
left = flatten(bst.left)
left.append(bst.val)
right = flatten(bst.right)
left.extend(right)
return bst, left # <-- add bst
def flatten(bst):
if bst is None:
return []
return step(bst)[-1]
But were stuck. We cant define get_parent because our tree data structure
doesnt keep track of parents, only children.
New plan: Maybe we can assume that someone has passed us the nodes parent and go from there?
But this plan hits the same brick wall: If we add a new argument to accept
the parent, we must for parallelism add a new return value to emit the
transformed parent, which is the parent of the parent. But we cant compute the parent of the parent because, as before, we have no way of implementing get_parent .
So we do what mathematicians do when their assumptions hit a brick wall:
we strengthen our assumption! Now we assume that someone has passed
us all of the parents, right up to the trees root. And that assumption gives us
what we need:
def step(bst, parents, left=None):
if left is None:
left = flatten(bst.left)
left.append(bst.val)
right = flatten(bst.right)
left.extend(right)
return parents[-1], parents[:-1], left
Note that were using the Python stack convention for parents ; thus the
immediate parent of bst is given by the final element parents[-1] .
As a simplification, we can eliminate the bst argument by considering it
the final parent pushed onto the stack:
def step(parents, left=None):
bst = parents.pop() # <-- bst = top of parents stack
if left is None:
left = flatten(bst.left)
left.append(bst.val)
right = flatten(bst.right)
left.extend(right)
return parents, left
Now that step requires the parents stack as an argument, the base function must provide it:
def flatten(bst):
if bst is None:
return []
parents = [bst]
return step(parents)[-1]
But we still havent eliminated the first recursive call. To do that, well need
to pass the step function a value for its left argument, which will cause
the recursive call to be skipped.
But we only know what that value should be for one case, the base case,
when bst is None ; then left must be [] . To get to that case from the trees
root, where bst is definitely not None , we must iteratively replicate the
normal recursive calls on bst.left until we hit the leftmost leaf node. And
then, to compute the desired result, we must reverse the trip, iterating the
step function until we have returned to the trees root, where the parents
stack must be empty:
def flatten(bst):
# find initial conditions for secret-feature "left"
left = []
parents = []
while bst is not None:
parents.append(bst)
bst = bst.left
# iterate to compute the result
while parents:
parents, left = step(parents, left)
return left
And just like that, one of the recursive calls has been transformed into iteration. Were halfway to the finish line!
(bst1, ) = (bst.right, )
parents1 = []
while bst1 is not None:
parents1.append(bst1)
bst1 = bst1.left
while parents1:
parents1, left = step(parents1, left)
# -- end partial evaluation -return parents, left
def flatten(bst):
left = []
parents = []
while bst is not None:
parents.append(bst)
bst = bst.left
while parents:
parents, left = step(parents, left)
return left
When flatten calls step and the code within the partially evaluated region
executes, it builds up a stack of nodes parents1 and then calls step iteratively to pop values off of that stack and process them. When its finished,
control returns to step proper, which then returns to its caller, flatten ,
with the values ( parents , left ). But look at what flatten then does with
parents : it calls step iteratively to pop values off of that stack and process
them in exactly the same way.
So we can eliminate the while loop in step and the recursive call! by returning not parents but parents + parents1 , which will make the while
loop in flatten do the exact same work.
def step(parents, left):
bst = parents.pop()
left.append(bst.val)
# -- begin partial evaluation -(bst1, ) = (bst.right, )
parents1 = []
while bst1 is not None:
parents1.append(bst1)
bst1 = bst1.left
# while parents1:
# <-- eliminated
#
parents1, left = step(parents1, left) #
# -- end partial evaluation -return parents + parents1, left # parents -> parents + parents1
And then we can eliminate parents1 completely by taking the values we
would have appended to it and appending them directly to parents :
def step(parents, left):
bst = parents.pop()
left.append(bst.val)
# -- begin partial evaluation -(bst1, ) = (bst.right, )
# parents1 = [] # <-- eliminated
while bst1 is not None:
parents.append(bst1) # parents1 -> parents
bst1 = bst1.left
# -- end partial evaluation -return parents, left # parents + parents1 -> parents
And now, once we remove our partial-evaluation scaffolding, our step
function is looking simple again:
def step(parents, left):
bst = parents.pop()
left.append(bst.val)
bst1 = bst.right
while bst1 is not None:
parents.append(bst1)
bst1 = bst1.left
return parents, left
For the final leg of our journey simplification lets inline the step logic
back into the base function:
def flatten(bst):
left = []
parents = []
while bst is not None:
parents.append(bst)
bst = bst.left
while parents:
parents, left = parents, left
bst = parents.pop()
left.append(bst.val)
bst1 = bst.right
while bst1 is not None:
parents.append(bst1)
bst1 = bst1.left
parents, left = parents, left
return left
Lets eliminate the trivial argument-binding and return-value assignments:
def flatten(bst):
left = []
parents = []
while bst is not None:
parents.append(bst)
bst = bst.left
while parents:
# parents, left = parents, left
bst = parents.pop()
left.append(bst.val)
bst1 = bst.right
while bst1 is not None:
parents.append(bst1)
bst1 = bst1.left
# parents, left = parents, left
return left
# = no-op
# = no-op
And, finally, factor out the duplicated while loop into a local function:
def flatten(bst):
left = []
parents = []
def descend_left(bst):
while bst is not None:
parents.append(bst)
bst = bst.left
descend_left(bst)
while parents:
bst = parents.pop()
left.append(bst.val)
descend_left(bst.right)
return left
And thats it! We now have a tight, efficient, and iterative version of our
original function. Further, the code is close to idiomatic.
Thats it for this time. If you have any questions or comments, just hit me
at @tmoertel or use the comment form below.
Thanks for reading!
to our caller. If were to make this work, then, the caller must be willing to
help us out. Thats where the trampoline comes in. Its our co-conspirator
in the plot to eliminate stack build-up.
The trampoline
Heres what the trampoline does:
1. It calls our function f , making itself the current caller.
2. When f wants to make a recursive tail call to itself, it returns the instruction call(f)(*args, **kwds) . The language runtime dutifully removes the current execution frame from the stack and returns control
to the trampoline, passing it the instruction.
3. The trampoline interprets the instruction and calls f back, giving it
the supplied arguments, and again making itself the caller.
4. This process repeats until f wants to return a final result z ; then it returns the new instruction result(z) instead. As before, the runtime removes the current execution frame from the stack and returns control
to the trampoline.
5. But now when the trampoline interprets the new instruction it will return z to its caller, ending the trampoline dance.
Now you can see how the trampoline got its name. When our function uses
a return statement to remove its own execution frame from the stack, the
trampoline bounces control back to it with new arguments.
Heres a simple implementation. First, we will encode our instructions to
the trampoline as triples. Well let call(f)(*args, **kwds) be the triple
(f, args, kwds) , and result(z) be the triple (None, z, None) :
def call(f):
"""Instruct trampoline to call f with the args that follow."""
def g(*args, **kwds):
return f, args, kwds
return g
def result(value):
"""Instruct trampoline to stop iterating and return a value."""
return None, value, None
Now well create a decorator to wrap a function with a trampoline that will
interpret the instructions that the function returns:
import functools
def with_trampoline(f):
"""Wrap a trampoline around a function that expects a trampoline."""
@functools.wraps(f)
def g(*args, **kwds):
h = f
# the trampoline
while h is not None:
h, args, kwds = h(*args, **kwds)
return args
return g
Note that the trampoline boils down to three lines:
while h is not None:
h, args, kwds = h(*args, **kwds)
return args
Basically, the trampoline keeps calling whatever function is in h until that
function returns a result(z) instruction, at which time the loop exits and z
is returned. The original recursive tail calls have been boiled down to a
while loop. Recursion has become iteration.
Example: factorial
To see how we might use this implementation, lets return to the factorial
example from the first article in our series:
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)
Step one, as before, is to tail-convert the lone recursive call:
def factorial(n, acc=1):
if n < 2:
return acc
return factorial(n - 1, acc * n)
Now we can create an equivalent function that uses trampoline idioms:
def trampoline_factorial(n, acc=1):
if n < 2:
return result(acc)
return call(trampoline_factorial)(n - 1, n * acc)
Note how the return statements have been transformed.
Finally, we can wrap this function with a trampoline to get a callable version that we can use just like the original:
factorial = with_trampoline(trampoline_factorial)
Lets take it for a spin:
>>> factorial(5)
120
To really see whats going on, be sure to use the Online Python Tutors visualizer to step through the original, tail-recursive, and trampoline versions
of the function. Just open this link: Visualize the execution. (ProTip: use a
new tab.)