A brief introduction to tail recursion

Recursion is an immensely powerful tool for a programmer. Like many abstractions, it allows us see past the weeds of a problem and strive for a more elegant understanding of the underlying logic. Unfortunately, simplifying something for a human frequently comes at the expense of the computer. In the case of recursion, your beautiful, recursive solution demands an extraordinary amount of memory.

The source of recursion’s memory woes is simple: each recursive call requires its own space in memory to maintain its own runtime environment. That environment holds crucial data, including local variables, the script itself, and, most importantly, the location in memory of the previous recursive call. The final piece of information is especially important for recursive calls. Without a return address, each layer of recursion would be left waiting forever for the next layer to finish.

Our concern over memory goes beyond hypothetical optimization. In some cases, the engine executing our code will flat-out refuse to allocate more memory for further recursive calls. Consider a simple example of a recursive algorithm.

function addToN(n){
if (n <= 0) return 0
else return n + addToN(n-1)
}

The function returns the sum of all numbers from 1 to n. So addToN(5) = 5 + 4+ 3 + 2 + 1 = 15. Easy! But what happens if we want to consider especially large inputs? For example, addToN(1000000) will require 1,000,000 recursive calls. Most languages, including JavaScript and Python, will return an error stating that the maximum recursion depth has been exceeded. Many programmers encounter a similar error when they first learn recursion but fail to properly implement a base case, resulting in an infinite loop. In this case, we know that the function should eventually finish, but it requires more memory than we can safely expect.

Enter tail recursion. A powerful technique that is fundamental to functional programming languages, tail recursion overcomes the memory demands of recursion by asking, “What if we never needed to return to the previous recursive call?” More to the point, what if we can continuously recycle the memory carved out for each recursive stack?

It’s a simple question, and the execution is similarly straightforward. To accomplish this goal, we need to rewrite our recursive function so that no information is required from the previous call. This is often accomplished with helper functions and with additional arguments. For the case above, lets develop a second helper function that takes a second argument.

function addToNWithSum(n, sum){      if (n==0) return sum      else return addToNWithSum(n-1, sum+n)
}
function addToN(n){ return addToNWithSum(n, 0)
}

And voila! Each recursive call makes the call to the next layer, but has no operations after that call is returned. An optimization engine can recognize this feature. Since no information from the previous call is required once the next call is made, that chunk of memory can be safely replaced. Rather than demanding 1,000,000 separate stacks in memory, addToN(1000000) now only requires 1, and our memory requirement is O(1).

Unfortunately, the key to this technique is the optimization process. If you are working in a language that does not include tail-call optimization (TCO), you are out of luck. Even if your final return value is just going to be passed back up the stack, your engine will dutifully preserve every recursive call until the bottom.

So where does JavaScript fall in terms of TCO? Currently, it is unfortunately implemented pretty rarely. While previous versions of Node.js (specifically Node 6.5–7.10) allowed TCO with the “ — harmony” flag in strict mode, Node 8 has dropped the feature. Similarly, Safari is the only major browser that currently supports TCO. Some programming languages that are more “rigorously” steeped in the functional programming philosophy, notably Haskell and Scheme, gleefully implement TCO. Compiled languages like C are also capable of TCO with appropriate compilers. A high enough level of optimization with gcc will enable TCO and allow arbitrarily deep levels of recursion.

Even if TCO does not provide an immediate benefit to memory usage in many languages, it is still a unique, clever way to describe recursive problems. Asking yourself if it is possible to refactor a recursive solution so that it implements tail recursion can often force you to reevaluate what information is passed from one recursive call to another. Since the ultimate goal of recursion is to break down problems into more conceptually digestible problems, it is a valuable exercise for describing your solution. The more practiced you become at reducing massive problems into simple, human-readable procedures, the more confident you will feel approaching any complicated logical question!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store