stack overflow
– where the beloved site of the same name, gets its name. Let’s investigate how this works.n!=n * (n-1) * ... * 2 * 1
. If the author cares to define the same function recursively you might seeNotice how the definition is made of two parts, a recursive part and a base-case part. This translates nicely to programming. In a functional language we might define the factorial function like the following.
with |**************** (0) ******************************** ->
******************** (1) **************** |n->n*factorial ( n) ******************** -**************** (1) ********************) tail recursive, might look like the follwing.The syntax is slightly different, but the structure is suprisingly similar. This function is however not tail recursive, it is merely recursive.
A slightly re-written version, whichis
)let(rec) ******************* (factorial acc n) *******************= match nwith |**************** (0) ******************************** ->
acc |n->factorial(
acc (n **********))(n
-1
This definition is mostly the same, with the addition of a new parameter
acc
, short for accumulator. Introducing an accumulator is often necessary to be able to rewrite a function to be recursive. Annoyingly, this version has to be called with an initial value ofacc=1
, so for instance (5!) *********** (would look likefactorial 1 5) .
The crucial difference between these definitions, however, is that the recursive call to
factorial
in this new definition is in "tail position". In a sense, the last operation that is performed in the body of the function definition, is to call itself. This is what allows the compiler to replace the function call with a loop instruction and reuse the same stack frame - because there is no more computation to be done in this stack frame after the recursive call. When it returns, all we do is return that answer. In which case we might as well return the answer directly.Maybe this becomes more clear if we take a look at the same two definitions re-written more verbosely, using intermediary variable names and if statements, instead of the more terse mathematics-like syntax above .
)let(rec) ******************* (factorial n)= ifn=(0) **************** then1 else let rest (=(factorial) **************** ( n) ******************** - (1) ********************) n********************* )let(rec) ******************* (factorial acc n) *******************= ifn=(0) **************** then acc elseletnext (=**************** n) ******************* - 1letacc (************ nsofar=factorial sofar next The important difference is that in the first definition there is more work to be done after the recursive call, namely the multiplication
n * rest, while in the second definition, all the work happens before.
One way I like to think about this, is in a mutable way. I think of the last line
factorial sofar next
as an instruction to go back to the top of the function and bindacc
to this new valuesofar
andn
to
next
. Whenevern==0
and it is time to break out of this looping recursion, we simply return the latest value ofacc
directly to the original caller of the factorial function.In fact, this is precisely how Clojure implements tail recursion. Instead of detecting it automatically, Clojure forces you to use a special syntax with the special keywords
loop
andrecur
. Here is a Clojure implementation.(defn[n] (**************** (loop) **************************** [acc1 n n] (**************** (if(=n****** (0) ******************** f0=>x0) acc (**************** (recur) **************************** (
*acc n)(
dec (n)
********************** )
Again we see the same structure, only expressed using a different syntax.
TrampoliningIn many popular languages such as Python and JavaScript there is no implementation of the language which implements tail recursion, so even if you write your functions in a tail recursive style, it has no effect.
We can however implement implement it ourselves if we want to do many and deep recursions, without blowing the stack, using a technique known astrampolining. In this technique we write our functions in a tail recursive style, but instead of evaluating our function recursively before returning, we instead return the function we want called and the argument we want to call it with. Then we wrap this in a loop which is responsible for calling it repeatedly, until the function signals that it is done. We signal this by returning a special value, such as undefined, instead of the function, when we reach the base case.
Here is an implementation in JS.
constfactorial=(****************** ()**************
************************)=>{ if**************** ()===(0) **************** ){ return**************** [undefined,acc]
; } return******************
******************]**************;}*******************;const**************** (trampoline*********************=
> { let f (=) **************;let
x=x0 (*******************; while****************** (**************=****************************** (undefined) **************){ =****************** (f) ************************************ (x ; } returnx;}*******************;
constfact=(trampoline) ****************
() Factorial);
fact
can now be called with
[1, 5]
to evalueate the factorial of 5
. One thing to note about this example is the use of two-element lists to represent tuples. The return value offactorial
is a tuple of the function reference (or undefined) and a tuple of the parameters to the function . The parameters of the function is the accumulator and the current value to compute the factorial of.Notice also the destructuring of
f
and
x
in the while loop, which also re-assigns the variables of the same names. For every iteration of the loop,x
is the next set of parameters the function will be called with. When the loop is finished and the recursion done, the value of x is the final value of the computation, the logical return value of the recursive function.
****************
****************************** (
GIPHY App Key not set. Please check settings