The solution? A process known as trampolining. It's a way to "hack" the concept of tail-call elimination into a program by using thunks.
Thunks are, for this purpose, expressions with arguments that wrap anonymous functions with no arguments of their own. For example: function(str){return function(){console.log(str)}}. This prevents the expression from being evaluated until a receiving function calls the anonymous function.
A trampoline is a function that takes a function as input and repeatedly executes its returned value until something other than a function is returned. A simple implementation is shown in the following code snippet:
var trampoline = function(f) { while (f && f instanceof Function) { f = f.apply(f.context, f.args); } return f; }To actually implement tail-call elimination, we need to use thunks. For this, we can use the bind() function that allows us to apply a method to one object with the this keyword assigned to another. Internally, it's the same as the call keyword, but it's chained to the method and returns a new bound function. The bind() function actually does partial application, though in a very limited way.
var factorial = function(n) { var _fact = function(x, n) { if (n == 0) { // base case return x; } else { // recursive case return _fact.bind(null, n*x, n-1); } } return trampoline(_fact.bind(null, 1, n)); }But writing the fact.bind(null, ...) method is cumbersome and would confuse anybody reading the code. Instead, let's write our own function for creating thunks. There are a few things the thunk() function must do:
thunk() function must emulate the _fact.bind(null, n*x, n-1) method that returns a non-evaluated functionthunk() function should enclose two more functions:With that, we're ready to write the function. We only need a few lines of code to write it.
var thunk = function (fn) { return function() { var args = Array.prototype.slice.apply(arguments); return function() { return fn.apply(this, args); }; }; };Now we can use the thunk() function in our factorial algorithm like this:
var factorial = function(n) { var fact = function(x, n) { if (n == 0) { return x; } else { return thunk(fact)(n * x, n - 1); } } return trampoline(thunk(fact)(1, n)); }But again, we can simplify it just a bit further by defining the _fact() function as a thunk() function. By defining the inner function as a thunk() function, we're relieved of having to use the thunk() function both inside the inner function definition and in the return statement.
var factorial = function(n) { var _fact = thunk(function(x, n) { if (n == 0) { // base case return x; } else { // recursive case return _fact(n * x, n - 1); } }); return trampoline(_fact(1, n)); }The result is beautiful. What seems like the function _fact() being recursively called for a tail-free recursion is almost transparently processed as an iteration!
Finally, let's see how the trampoline() and thunk() functions work with our more meaningful example of tree traversal. The following is a crude example of how a data tree could be traversed using trampolining and thunks:
var treeTraverse = function(trunk) { var _traverse = thunk(function(node) { node.doSomething(); node.children.forEach(_traverse); } trampoline(_traverse(trunk)); }We've solved the issue of tail recursion. But is there an even better way? What if we could simply convert the recursive function to a non-recursive function? Up next, we'll look at how to do just that.