Thanks, this is very interesting. I am enrolled in a CS Bsc. (night courses) and while we learned about the different techniques of recursive programming, we never learned how to convert a recursive solution into an iterative one.
I remember when I was at college it was the same, and it doesn't make any sense. I can't recall any instances where I've needed to convert an iterative solution into a resursive one, but there have been countless times I've done the reverse. I belive that the mindless emphasis on resursive programming in college courses has lead to poor practices, where people are using recursion to just make their code more elegant where an iterative solution wouldn't much more complicated but a lot more robust. The result? Down the track, an unforseen use cases resulting in max stack size exceeded errors, causing needless headaches.
> I belive that the mindless emphasis on resursive programming in college courses has lead to poor practices, where people are using recursion to just make their code more elegant
Most courses, especially introductory ones, don’t have particularly good examples of when to use recursion. This doesn’t mean that there aren’t cases where recursion isn’t useful, though.
I think the prevalence of recursion might be attributable to mathematics. A lot of mathematical analysis of algorithms is more natural to express in terms of recursion (partially because of proof by induction), so you often see algorithms stated that way.
It's also worth noting that there are plenty of instances where (IMO), recursion is much more natural. For example, traversing trees recursively. I agree, though, there should definitely be more time spent teaching how to convert between the two.
One of the reasons I wrote this article was because of exactly the problem that you mention, so I am very happy that you found it interesting! I frequently interview recent CS graduates, and ask a technical question about techniques for removing recursions; the people who do well on such questions tend to have practical experience writing compilers. A lot of these basic techniques are not covered in undergraduate courses, but they are crucial to understanding how modern programming languages work under the hood. I still have my notes from my fourth year course in continuation passing style from my undergrad days and it was very helpful to me when we started adding these concepts to C#.
The article doesn't explain the problem of converting recursion to iteration fully. There is a lot more detail.
Most recursion can be implemented using a loop and a stack. If the recursive expression is self contained in the return call, then you don't need a stack.
For example:
def somefunc(x):
if x <= 0:
return 0
return somefunc(x-1)
is equal to:
def somefunc(x)
while x > 0:
x = x - 1
return x
But if the recursion isn't self contained:
def somefunc(x):
if x == 0:
return 0
return somefunc(x-1) + 1
then the iterative version
looks like this:
def somefunc(x)
stack = []
while x > 0:
stack.append(x)
x = x - 1
while len(stack) != 0:
x = x + stack[-1]
stack = stack[:-1]
return x
During Recursion your program will use an internal stack to keep track of previous context. This is called the call stack. Whether or not you use iteration or recursion for the last example you will need a stack.
The last example is actually less efficient than the recursive version because you are moving the call stack to the heap. The heap is slower than the call stack but the heap essentially has no limits so you won't encounter stack overflow.
In the first example of recursion you will actually benefit from conversion to iteration because you get rid of the stack all together in the iterative example. Some languages will be able to recognize this type of recursion and do an optimization step called "tail-recursion optimization" in which the compiler actually gets rid of the need for a the call stack. Python does not do this, however.
The story doesn't end here, however. There are examples of recursion where you cannot create a straightforward iterative version.
For example:
def somefunc(x):
if x <= 0:
return 0
return somefunc(x-1) + somefunc(x-2)
The straight forward conversion using an explicit stack looks like this:
def somefunc(x)
stack = []
while x > 0:
stack.append(lambda: somefunc(x-2))
x = x - 1
while len(stack) != 0:
x + stack[:-1]()
stack = stack[:-1]
return x
You will note that the straightforward conversion doesn't get rid of recursion. The reason is because the previous context is basically another recursive call.
You will note that the recursive version of this function actually does the same multiple recursive calls twice which leads to unnecessary repeated calculations. It is possible to optimize this repetition using something called memoization. However memoization requires and does not get rid of recursion.
It is not possible to imitate this "inn-efficiency" in an iterative form. The only way to do it is to do an optimization step similar to memoization before iterating. This step, however radically changes the way you think about the problem. It literally reverses the recursion from a top down approach to a bottom up approach. Here is what the iterative form looks like:
def somefunc(x):
store = [None for _ in range(x)]
store[0] = 0
for i in range(1, len(store)):
store[i] = store[i-1] + store[i-2] if i-2 >= 0 else 0
return store[-1]
This method is called "Dynamic programming using the tabular method."
You will note that this version of the function still allocates storage to the heap with "store"
There is still a further optimization step you can do here as well. This optimization is basically noticing that you don't need to memorize the entire store in the algorithm. You only need the previous two values.
def somefunc(x):
store_i_minus_1 = 0
store_i_minus_2 = 0
for i in range(1, len(store)):
result = store_i_minus_1 + store_i_minus_2
...
Actually the example given in the article cannot be further optimized, even when using dynamic programming.
original:
def cost(s):
if s <= 1:
return 0
if s % 2 == 0:
return 1 + cost(s // 2)
return min(1 + cost(s - 1), 5)
tabular:
def cost(s):
if s <= 1:
return 0
store = [None for _ in range(s+1)]
store[0] = 0
store[1] = 0
for i in range(2, s+1):
store[i] = 1 + store[i//2] if i%2 == 0 else min(1 + store[i-1], 5)
return store[-1]
Because of this step: store[i//2], the algorithm reaches back into the store dynamically meaning that there is no fixed amount of memory that the algorithm reaches back to retrieve old results. I cannot just save the old results in a fixed size list or fixed amount of variables. So this is the furthest you can go following the tabular path.
This is how most interpreted languages work. The actual hardware stack (e.g. what $rsp points to) isn’t really relevant to the Python program being executed; it’s used by the native code in the interpreter itself. Pushing a Python stack frame intermingled with a hardware stack frame would be odd (and would likely be hard to do, and weird from an ABI perspective). It’s just easier to push the call stack to a dedicated section on the heap; the Python program doesn’t know the difference anyways. Plus, you can have a much larger stack by heap allocating it.
This seems like a very verbose, kinda-sorta continuation passing approach. Except that he's building the eventual continuation as a list of primitive functions instead of a complex function.
Ok, so a CPS transform starts by adding a new parameter, c, which I read "myReturn" or more likely, "returnapotamus". Then, you find the returns in the function and look at the expressions therein. An expression inside cost(...) is left alone, but the expression outside the application of cost is turned into an expression and then to a function that includes a call to returnapotamus, c:
The initial continuation is just the identity function. cost2 works the same as cost, but it doesn't solve our recursion problem. To do that, we can take Lipport's approach of a pseudo-function built from a stack of primitive functions:
def cost3(s):
cont = [ lambda s: s ]
while s > 1:
if s % 2 == 0:
cont.append(lambda s: s + 1)
s = s // 2
else:
cont.append(lambda s: min(s + 1, 5))
s = s - 1
result = 0
while len(cont) > 0:
result = cont.pop()(result)
return result
On the other hand, it's possible to build the continuation as a function, as we go, but it gets a little complicated:
def cost5(s):
cont = lambda s: s
while s > 1:
if s % 2 == 0:
cont = lambda s, c=cont: c(s + 1)
s = s // 2
else:
cont = lambda s, c=cont: c(min(s + 1, 5))
s = s - 1
return cont(0)
An explanation of what's going on here is beyond the scope of this comment.
I'm pretty sure that the words "tail recursion" or "tail call" should appear somewhere in this article - see e.g. https://en.wikipedia.org/wiki/Tail_call which also explains in a remarkably simple way how tail recursion relates to iterative constructs (as are found, e.g. in Python). Why do people write 'introductory' articles that don't even mention common terminology?
I discuss tail calls in part two. I am often surprised by the number of people who will criticize part one of an article for omitting certain details before reading the second part. Was "part one" unclear in some way?
I don't really get the point of this. As far as I can tell, the code is doing the same thing as before but has become (at least to me) much harder to understand.
If the argument to the original function is too large, the function runs out of stack space for the recursion. The function at the end of the post handles much larger arguments.
The point is to demonstrate the general truth that any method with a single recursion can be turned into a loop by making a series of small, safe program transformations. It is not intended to be a demonstration of a technique for writing more understandable code.
Am I the only one who read this and thought: hmm, for any number 32 or higher, the minimum cost would always be 5? Who cares about recursion when it's such a constrained problem? :)
It's not as simple as that -- compute the cost of 2^64, 2^64-1, and 2^64-64 to see some more interesting behavior... (I'm using ^ because HN eats double-asterisks; your python may vary)
You are not the only one who thought that; see the comments to my post. As I note in part two, the point of the article was to show the general method; there are of course many ways to solve the original problem more efficiently. The specific problem is trivial and unimportant.
30 comments
[ 1.6 ms ] story [ 67.0 ms ] threadMost courses, especially introductory ones, don’t have particularly good examples of when to use recursion. This doesn’t mean that there aren’t cases where recursion isn’t useful, though.
It's also worth noting that there are plenty of instances where (IMO), recursion is much more natural. For example, traversing trees recursively. I agree, though, there should definitely be more time spent teaching how to convert between the two.
Most recursion can be implemented using a loop and a stack. If the recursive expression is self contained in the return call, then you don't need a stack.
For example:
is equal to: But if the recursion isn't self contained: then the iterative version looks like this: During Recursion your program will use an internal stack to keep track of previous context. This is called the call stack. Whether or not you use iteration or recursion for the last example you will need a stack.The last example is actually less efficient than the recursive version because you are moving the call stack to the heap. The heap is slower than the call stack but the heap essentially has no limits so you won't encounter stack overflow.
In the first example of recursion you will actually benefit from conversion to iteration because you get rid of the stack all together in the iterative example. Some languages will be able to recognize this type of recursion and do an optimization step called "tail-recursion optimization" in which the compiler actually gets rid of the need for a the call stack. Python does not do this, however.
The story doesn't end here, however. There are examples of recursion where you cannot create a straightforward iterative version.
For example:
The straight forward conversion using an explicit stack looks like this: You will note that the straightforward conversion doesn't get rid of recursion. The reason is because the previous context is basically another recursive call.You will note that the recursive version of this function actually does the same multiple recursive calls twice which leads to unnecessary repeated calculations. It is possible to optimize this repetition using something called memoization. However memoization requires and does not get rid of recursion.
It is not possible to imitate this "inn-efficiency" in an iterative form. The only way to do it is to do an optimization step similar to memoization before iterating. This step, however radically changes the way you think about the problem. It literally reverses the recursion from a top down approach to a bottom up approach. Here is what the iterative form looks like:
This method is called "Dynamic programming using the tabular method."You will note that this version of the function still allocates storage to the heap with "store"
There is still a further optimization step you can do here as well. This optimization is basically noticing that you don't need to memorize the entire store in the algorithm. You only need the previous two values.
original:
tabular: Because of this step: store[i//2], the algorithm reaches back into the store dynamically meaning that there is no fixed amount of memory that the algorithm reaches back to retrieve old results. I cannot just save the old results in a fixed size list or fixed amount of variables. So this is the furthest you can go following the tabular path.Is this true in Python? I thought the call stack was on the heap as well.
Ok, so a CPS transform starts by adding a new parameter, c, which I read "myReturn" or more likely, "returnapotamus". Then, you find the returns in the function and look at the expressions therein. An expression inside cost(...) is left alone, but the expression outside the application of cost is turned into an expression and then to a function that includes a call to returnapotamus, c:
The initial continuation is just the identity function. cost2 works the same as cost, but it doesn't solve our recursion problem. To do that, we can take Lipport's approach of a pseudo-function built from a stack of primitive functions: On the other hand, it's possible to build the continuation as a function, as we go, but it gets a little complicated: An explanation of what's going on here is beyond the scope of this comment.Converting to continuation passing does result in tail calls.
The presentation is very complicated, though.