Книга: Functional Programming in JavaScript
Назад: Tail recursion
Дальше: The Y-combinator

Trampolining

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.

Note

Thunks are, for this purpose, expressions with arguments that wrap anonymous functions with no arguments of their own. For example: . 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 function that allows us to apply a method to one object with the keyword assigned to another. Internally, it's the same as the keyword, but it's chained to the method and returns a new bound function. The 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 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 function must do:

  • function must emulate the method that returns a non-evaluated function
  • The function should enclose two more functions:
    • For processing the give function, and
    • For processing the function arguments that will be used when the given function is invoked

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 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 function as a function. By defining the inner function as a function, we're relieved of having to use the 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 being recursively called for a tail-free recursion is almost transparently processed as an iteration!

Finally, let's see how the and 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.

Назад: Tail recursion
Дальше: The Y-combinator

bsn
thank
Vesa Karvonen
I hope you don't mind, but I’d like to point you and your readers to my high-performance optics library for JavaScript that is in production use in multiple projects, has comprehensive support for partial optics and interactive documentation: https://calmm-js.github.io/partial.lenses/ (this takes a moment to load — be patient!)