Tail recursion in Python, part 1: trampolines

In this blog post I will try to explain what tail recursion is and show
how tail call optimization can be achieved in Python using trampolines.

Python is not a functional programming language, but allows you
to write the programs in functional way.

Functional programming is based on these principles:

Functions are first class citizens - you can define functions
and call them, but more importantly you can pass the functions
as parameters to another function. Also you can return
a function as a result of calling another function.

Referential transparency & immutable data structures - functions have
no "side-effects" e.g. calling given function many times with the same
parameters will give the same results. Also, there is no way
to re-assign variable to new value.

Recursion is allowed, e.g. function can call itself.

The recursion is required because with referential transparency you can't
use loops.

The problem with recursion is that each function call requires
to store some data which is required to restore the state when
the function call finishes. This is problematic when you want do
a lot of nested calls, because it may lead to infamous
stack overflow.

To counteract this, many functional languages (like Haskell or OCaml)
can transform function written as tail-recursive to a function
internally running a loop.
This process is called tail call optimization.

The function is tail-recursive only when it:

returns direct result of recursive call to itself

does not do any recursive call besides point 1

This causes the
call graph (calling context tree)
of recursive calls to be linear.
And because we return the direct result of recursive call we don't need
in theory to store the state of function when we doing a recursive call,
because when the call finishes we don't need to restore the state,
we just return the computed value returned by called function.

Therefore we can simulate performing these nested recursive calls
as a loop, where function parameters become variables,
changed in each iteration.

The example

Let's try some example: We will try to calculate the Fibonacci sequence,
defined as below:

As we can see, this function is not tail-recursive, because we don't return
direct result of recursive call of fib but result of sum of
two recursive calls. So even if Python supported
tail call optimization out-of-the-box it wouldn't be tail call optimized.

Ooops! It seems that we stumbled upon the previously mentioned
stack overflow.
In other words: each nested function call needs to store some data so the
state of the program can be properly restored when given function finishes.
The space which stores this kind of information is limited,
so there is a limited depth of function calls.

Let's try to rewrite the function in a tail-recursive fashion.
To do this, we need to re-think how to calculate next Fibonacci number.
We can do that by tracking the computation of two consecutive Fibonacci numbers.

With that, we actually optimized the function. Now fib(n)time complexity is linear
(O(n)O(n)O(n) in Big O notation)
instead of being exponential (O(2n)O(2^n)O(2n)).

Also, the internal helper function is actually in tail-recursive form,
because it returns the direct result of
recursive call - helper(k - 1, a2, b2).
The fib functions only primes the helper function with start parameters.

The problem is that the helper(k - 1, a2, b2) call in the lambda expression
refers to the decorated function, not the original function.
Therefore instead of one while loop, we get multiple while loops,
each performing just one step.