Kevin succinctly points out how this particular code snippet works (along with why it's quite incomprehensible), but I wanted to add some information about how trampolines in general work.
Without tail-call optimization (TCO), every function call adds a stack frame to the current execution stack. Suppose we have a function to print out a countdown of numbers:
function countdown(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
countdown(n - 1);
}
}
If we call countdown(3)
, let's analyze how the call stack would look without TCO.
> countdown(3);
// stack: countdown(3)
Launch in 3
// stack: countdown(3), countdown(2)
Launch in 2
// stack: countdown(3), countdown(2), countdown(1)
Launch in 1
// stack: countdown(3), countdown(2), countdown(1), countdown(0)
Blastoff!
// returns, stack: countdown(3), countdown(2), countdown(1)
// returns, stack: countdown(3), countdown(2)
// returns, stack: countdown(3)
// returns, stack is empty
With TCO, each recursive call to countdown
is in tail position (there's nothing left to do other than return the result of the call) so no stack frame is allocated. Without TCO, the stack blows up for even slightly large n
.
Trampolining gets around this restriction by inserting a wrapper around the countdown
function. Then, countdown
doesn't perform recursive calls and instead immediately returns a function to call. Here's an example implementation:
function trampoline(firstHop) {
nextHop = firstHop();
while (nextHop) {
nextHop = nextHop()
}
}
function countdown(n) {
trampoline(() => countdownHop(n));
}
function countdownHop(n) {
if (n === 0) {
console.log("Blastoff!");
} else {
console.log("Launch in " + n);
return () => countdownHop(n-1);
}
}
To get a better sense of how this works, let's look at the call stack:
> countdown(3);
// stack: countdown(3)
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(3)
Launch in 3
// return next hop from countdownHop(3)
// stack: countdown(3), trampoline
// trampoline sees hop returned another hop function, calls it
// stack: countdown(3), trampoline, countdownHop(2)
Launch in 2
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(1)
Launch in 1
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(0)
Blastoff!
// stack: countdown(3), trampoline
// stack: countdown(3)
// stack is empty
At each step the countdownHop
function abandons direct control of what happens next, instead returning a function to call that describes what it would like to happen next. The trampoline function then takes this and calls it, then calls whatever function that returns, and so on until there is no "next step". This is called trampolining because the flow of control "bounces" between each recursive call and the trampoline implementation, instead of the function directly recurring. By abandoning control over who makes the recursive call, the trampoline function can ensure the stack doesn't get too large. Side note: this implementation of trampoline
omits returning values for simplicity.
It can be tricky to know whether this is a good idea. Performance can suffer due to each step allocating a new closure. Clever optimizations can make this viable, but you never know. Trampolining is mostly useful for getting around hard recursion limits, for instance when a language implementation sets a maximum call stack size.
loopy
doesn't overflow because it doesn't call itself.