Sunday , May 16 2021

Tail recursion, Hacker News


Tail recursion is a special way of writing recursive functions such that a compiler can optimize the recursion away and implement the algorithm as a loop instead. This is not because loops are inherently faster, but because every function call generally incurs the cost of a new stack frame in which it executes, sometimes leading to the dreadedstack overflow– where the beloved site of the same name, gets its name. Let’s investigate how this works.

But first we need to consider what recursion is and which functions are tail recursive and which are not. A function is said to be recursive if it is defined in terms of itself. Not only is recursion a natural and elegant solution for many problems, but in pure functional languages ​​such as Elm and Haskell, there is simply no other way to express looping. As is customary when thinking about recursion, we will look at the factorial function. In mathematics we might see a definition such asn!=n * (n-1) * ... * 2 * 1. If the author cares to define the same function recursively you might see

n!=n * (n-1)! if n>0 n!=1 if n=0

Notice 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.

)let(rec) ******************* (factorial n)=    match

n

with    |**************** (0) ******************************** ->

******************** (1) ****************     |n->n*factorial  ( n) ******************** -
**************** (1) ********************)

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

tail recursive, might look like the follwing.

)let(rec) ******************* (factorial acc n) *******************=    match

n

with    |**************** (0) ******************************** ->

acc     |n->factorial(
acc (n **********))
(

n

-
1

This definition is mostly the same, with the addition of a new parameteracc, 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 tofactorialin 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     else      
let
sofar
=
acc (************ n       
letnext (=**************** n) ******************* - 1
      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 linefactorial sofar nextas an instruction to go back to the top of the function and bindaccto this new valuesofarandntonext. Whenevern==0and it is time to break out of this looping recursion, we simply return the latest value ofaccdirectly 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 keywordsloopandrecur. Here is a Clojure implementation.

(
defn[n]  (**************** (loop) **************************** [acc1 n n]    (**************** (if(
=n****** (0) ********************
)      acc      (**************** (recur) **************************** (
*acc
n
)

(

dec (n)

**********************

)

Again we see the same structure, only expressed using a different syntax.

Trampolining

In 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*********************=

f0=>x0

>

{   let

f (=) **************;

  let

x

=x0 (*******************;   while****************** (**************=****************************** (undefined) **************){    =****************** (f) ************************************ (x ;  }   returnx;}*******************;

const

fact

=(trampoline) ****************() Factorial);

factcan now be called with[1, 5]

to evalueate the factorial of5. One thing to note about this example is the use of two-element lists to represent tuples. The return value offactorialis 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 off

andxin the while loop, which also re-assigns the variables of the same names. For every iteration of the loop,xis 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.

****************

****************************** (

***************************** Read More

******************************

About admin

Leave a Reply

Your email address will not be published. Required fields are marked *