GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing inventwithpython.com — Chapter 8 - Tail Call Optimization Check out other books by Al Sweigart, free online or available for purchase: ...and other books as well! Or register for the online video course. You can also donate to support the author directly.
In the previous chapter, we covered using memoization to optimize recursive functions. This chapter explores a technique called tail call optimization, which is a feature provided by a compiler or interpreter to avoid stack overflows. Tail call optimization is also called tail call elimination, or tail recursion elimination. This chapter is meant to explain tail call optimization, not to endorse it. I would go so far as to recommend never using tail call optimization. As you’ll see, rearranging your function’s code to use tail call optimization often makes it far less understandable. You should consider tail call optimization to be more of a hack or workaround to make recursion work when you shouldn’t be using a recursive algorithm in the first place. Remember, a complex recursive solution is not automatically an elegant solution; simple coding problems should be solved with simple non-recursive methods. Many implementations of popular programming languages don’t even offer tail call optimization as a feature. These include interpreters and compilers for Python, JavaScript, and Java. However, tail call optimization is a technique you should become familiar with in case you come across it in the code projects you work on. How Tail Recursion and Tail Call Optimization WorkTo make use of tail call optimization, a function must use tail recursion. In tail recursion, the recursive function call is the last action of a recursive function. In code, this looks like a To see this in action, recall the factorialByRecursion.py and factorialByRecursion.html programs in Chapter 2. These programs calculated the factorial of an integer; for instance, 5! is equivalent to 5 × 4 × 3 × 2 × 1, or 120. These calculations can be performed recursively because Let’s rewrite these programs to use tail recursion. The following factorialTailCall.py program has a Python
The factorialTailCall.html program has the equivalent JavaScript code: JavaScript
Notice that the To understand how tail call optimization works, remember from Chapter 1 what happens when a function is called. First, a frame object is created and stored on the call stack. If the function call calls another function, another frame object is created and placed on top of the first frame object on the call stack. When a function returns, your program automatically deletes the frame object from the top of the stack. A stack overflow happens when too many function calls are made without returning, causing the number of frame objects to exceed the capacity of the call stack. This capacity is 1,000 function calls for Python and about 10,000 for JavaScript programs. While these amounts are more than enough for typical programs, recursive algorithms could exceed this limit and cause a stack overflow that crashes your program. Recall from Chapter 2 that a frame object stores the local variables in the function call as well as the return address of the instruction to return to when the function finishes. However, if the last action in the recursive case of a function is to return the results of a recursive function call, there’s no need to retain the local variables. The function does nothing involving the local variables after the recursive call, so the current frame object can be deleted immediately. The next frame object’s return address information can be the same as the old deleted frame object’s return address. Since the current frame object is deleted instead of retained on the call stack, the call stack never grows in size and can never cause a stack overflow! Recall from Chapter 1 that all recursive algorithms can be implemented with a stack and a loop. Since tail call optimization removes the need for a call stack, we are effectively using recursion to simulate a loop’s iterative code. However, earlier in this book I stated that the problems suitable for recursive solutions involve a tree-like data structure and backtracking. Without a call stack, no tail recursive function could possibly do any backtracking work. In my view, every algorithm that can be implemented with tail recursion would be easier and more readable to implement with a loop instead. There’s nothing automatically more elegant about using recursion for recursion’s sake. Accumulators in Tail RecursionThe disadvantage of tail recursion is that it requires rearranging your recursive function so that the last action is returning the recursive call’s return value. This can make our recursive code even more unreadable. Indeed, the In the case of our tail call Also notice that the base case for You can think of the
Figure 8-1: The process of transformations that Rearranging your recursive calls as the last action in the function and adding accumulators can make your code even harder to understand than typical recursive code. But that’s not the only downside of tail recursion, as we’ll see in the next section. Limitations of Tail RecursionTail recursive functions require rearranging their code to make them suitable for the tail call optimization feature of the compiler or interpreter. However, not all compilers and interpreters offer tail call optimization as a feature. Notably, CPython (the Python interpreter downloaded from https://python.org) does not implement tail call optimization. Even if you write your recursive functions with the recursive call as the last action, it will still cause stack overflows after enough function calls are made. Not only that, but CPython will likely never have tail call optimization as a feature. Guido van Rossum, the creator of the Python programming language, has explained that tail call optimization can make programs harder to debug. Tail call optimization removes frame objects from the call stack, which in turn removes the debugging information that frame objects can provide. He also points out that once tail call optimization is implemented, Python programmers will begin to write code that depends on the feature, and their code won’t run on non-CPython interpreters that don’t implement tail call optimization. Finally, and I concur, van Rossum disagrees with the idea that recursion is a fundamentally important part of day-to-day programming. Computer scientists and mathematicians tend to place recursion on a pedestal. But tail call optimization is simply a workaround hack to make some recursive algorithms actually workable, rather than simply crashing with a stack overflow. While CPython doesn’t feature tail call optimization, this doesn’t mean another compiler or interpreter that implements the Python language couldn’t have tail call optimization. Unless tail call optimization is explicitly a part of a programming language specification, it is not a feature of a programming language, but rather of individual compilers or interpreters of a programming language. The lack of tail call optimization is not unique to Python. The Java compiler also doesn’t support tail call optimization. Tail call optimization is a part of the ECMAScript 6 version of JavaScript; however, as of 2022, only the Safari web browser’s implementation of JavaScript actually supports it. One way to determine whether your programming language’s compiler or interpreter implements this feature is to write a tail recursive factorial function and try to calculate the factorial of 100,000. If the program crashes, tail call optimization is not implemented. Personally, I take the stance that the tail recursion technique should never be used. As stated in Chapter 2, any recursive algorithm can be implemented with a loop and a stack. Tail call optimization prevents stack overflows by effectively removing the use of the call stack. Therefore, all tail recursive algorithms can be implemented with a loop alone. Since the code for loops is much simpler than a recursive function, a loop should be used wherever tail call optimization could be employed. Additionally, potential problems exist even if tail call optimization is implemented. Since tail recursion is possible only when the last action of a function is returning the recursive call’s return value, it’s impossible to do tail recursion for algorithms that require two or more recursive calls. For example, take the Fibonacci sequence algorithm calls Tail Recursion Case StudiesLet’s examine some of the recursive functions demonstrated earlier in this book to see if they are good candidates for tail recursion. Keep in mind that because Python and JavaScript do not actually implement tail call optimization, these tail recursive functions will still result in a stack overflow error. These case studies are for demonstration purposes only. Tail Recursive Reverse StringThe first example is the program to reverse a string that we made in Chapter 3. The Python code for this tail recursive function is in reverseStringTailCall.py: Python
The JavaScript equivalent is in reverseStringTailCall.html: JavaScript
The conversion of the original recursive functions in reverseString.py and reverseString.html involves adding an accumulator parameter. The accumulator, named By effectively using the accumulator as a local variable shared across function calls, we can make the
Figure 8-2: The process of transformations that Tail Recursive Find SubstringSome recursive functions naturally end up using the tail recursion pattern. If you look at the Tail Recursive ExponentsThe exponentByRecursion.py and exponentByRecursion.html programs, also from Chapter 2, are not good candidates for tail recursion. These programs have two recursive cases for when the However, notice the Python code for the Python
And notice the equivalent JavaScript recursive case: JavaScript
This recursive case does not have the recursive call as its last action. We could get rid of the
However, now we have two recursive calls to Tail Recursive Odd-EvenTo determine whether an integer is odd or even, you can use the Python
The JavaScript equivalent is in isOdd.html: JavaScript
We have two base cases for You can see why this works with an example: when However, this recursive function results in stack overflows for large-number arguments. Calling Python
The JavaScript equivalent is in the rest of isOdd.html: JavaScript
If this Python and JavaScript code is run by an interpreter that supports tail call optimization, calling If you think recursion, with or without tail recursion, is an incredibly inefficient way to determine whether a positive integer is odd, you are absolutely correct. Unlike iterative solutions, recursion can fail from stack overflows. Adding tail call optimization to prevent stack overflows doesn’t fix the efficiency flaws of using recursion inappropriately. As a technique, recursion is not automatically better or more sophisticated than iterative solutions. And tail recursion is never a better approach than a loop or other simple solution. SummaryTail call optimization is a feature of a programming language’s compiler or interpreter that can be employed on recursive functions specifically written to be tail recursive. Tail recursive functions return the return value of the recursive function call as the last action in the recursive case. This allows the function to delete the current frame object and prevent the call stack from growing as new recursive function calls are made. If the call stack doesn’t grow, the recursive function can’t possibly cause a stack overflow. Tail recursion is a workaround that allows some recursive algorithms to work with large arguments without crashing. However, this approach requires rearranging your code and possibly adding an accumulator parameter. This could make your code harder to understand. You may likely find that sacrificing code readability is not worth using a recursive algorithm over an iterative one. Further ReadingStack Overflow (the website, not the programming error) has a detailed discussion about the basics of tail recursion at https://stackoverflow.com/questions/33923/what-is-tail-recursion. Van Rossum wrote about his decision not to use tail recursion in two blog posts at https://neopythonic.blogspot.com.au/2009/04/tail-recursion-elimination.html and https://neopythonic.blogspot.com.au/2009/04/final-words-on-tail-calls.html. Python’s standard library includes a module called Practice QuestionsTest your comprehension by answering the following questions:
|
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |