If a function calls itself recursively then the JavaScript engine has to create a new 'stack' (i.e. allocate a chunk of memory) to keep track of the function's arguments.
Let's look at an example of this happening:
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 10); // => 11In the above code, the JavaScript engine has to create a new stack for each recursive call.
This happens because the original call to the sum function (i.e. sum(1, 10)) doesn't complete until the very last call to sum returns the value of x. Then x is returned to the caller and that caller returns x to its caller, all the way back to the very first call to sum where by it returns the result of the line return sum(x + 1, y - 1); (which ultimately was the value of x passed along from the last call to sum).
If we create a stack deep enough (e.g. sum(1, 100000)) then the JavaScript engine will throw a RangeError: Maximum call stack size exceeded.
The fix to this problem is to use fewer stacks.
To achieve this we could rewrite the code to not be recursive; so in other words: use a loop!
The problem with using a loop is that it's not as elegant (or clean/easy to read) as the recursive style of functional programming.
Another solution is to use a type of functional pattern called "trampolining". Let's take a look at one implementation of it...
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
}
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000) // => 100001Here we've written a tco function which wraps around the same code we had previously.
This could take some time to wrap your head around (lord knows it took me long enough), so if you don't get it the first time around then it's probably best to execute the above code via your browsers developer tools and use a debugger statement to step through the code whilst reading this explanation...
Note: the above code is an abstraction I found here: https://gist.github.com/Gozala/1697037. It makes tail call optimising any function really easy.
- We store the result of calling
tco(wrapped around our code) into thesumvariable. - The
sumvariable is now a function expression that when called (e.g.sum(1, 10)) will execute theaccumulatorfunction thattcoreturned. - The
accumulatoris a closure (meaning when we callsumtheaccumulatorwill have access to the variables defined inside oftco->value,activeandaccumulated; as well as our own code which is accessible via the identifierf). - When we call
sumfor the first time (e.g.sum(1, 10)) we indirectly executeaccumulator. - The first thing we do inside of
accumulatoris push the arguments object (and Array-like object) into theaccumulatedArray (so theaccumulatedwill now have a length of 1). - It's worth knowing that the
accumulatedArray only ever has 1 item inside of it (as we'll soon see). - The
activevariable by default isfalse. So asaccumulatoris called for the first time, we end up inside theifconditional, and then resetactivetotrue. - Now we get to a
whileloop (we're still calling functions recursively, as you'll see in a moment; but this is a very clean loop compared to an ugly for loop with lots of operators/operands). - The
whileloop simply checks whether theaccumulatedArray has any content. If it does then we call theffunction and pass through the arguments that were insideaccumulated[0](for the first run of this function that would've been[1, 10]). - When we pass the contents of
accumulated[0]we use theshiftArray method to actually remove it from theaccumulatedArray so it now has a length of zero. - If we ignore for a moment the code inside the loop; on the next iteration of this loop the condition of
accumulated.lengthwill fail and so we would end up settingactivetofalseand ultimately return tosumthe value of the variablevalue. - This is where it gets confusing, but hold tight!
- The
ffunction is our own code. Our own code calls thesumfunction (which indirectly calls theaccumulatorfunction).
The secret sauce to this entire chunk of code is coming up!
- If our code returns
xthen thewhileloop will stop (I'll explain why in a moment). - If our code can't return
x(notice our code has a conditional check to see ifyis greater than zero, if it is then we returnx, otherwise...) we callsumagain and pass through modified arguments. - This time when we call
sumwe don't get inside of theifconditional becauseactiveis already set totrue. - So the purpose of calling
sumfrom inside our own code is simply to allow us to store the newly modified arguments inside theaccumulatedArray. - The
sumfunction then returnsundefined(as we weren't able to move into theifconditional) - The flow of control then throws us back into the original
whileloop (that's still going, it hasn't stopped yet) andundefinedis what's stored into thevaluevariable. - At this point the
accumulatedArray has once again got some content and so thewhileloop's condition passes once more. - And that is where the recursive magic happens. At some point our code is going to call
sumwith theyargument set to zero meaning that when theaccumulatorfunction calls our code the conditiony > 0will fail and so we return the value ofx(which has been incremented each time along the way). - When that happens,
xis returned and assigned as the value to thevaluevariable, and so our code never calledsumand thus theaccumulatedArray is never modified again; meaning thewhileloop condition inside theaccumulatorfunction will fail and thus theaccumulatorfunction will end returning whatever value is stored inside thevaluevariable (which in this example is the value ofx).
There is another implementation I've seen which is much simpler to understand but not necessarily as easy to abstract like the tco method above.
Let's take a look at the code first (note: my explanation assumes an understanding of the this keyword and changing its context, if you're unsure then read more about it here):
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
function sum(x, y) {
function recur(x, y) {
if (y > 0) {
return recur.bind(null, x + 1, y - 1);
} else {
return x;
}
}
return trampoline(recur.bind(null, x, y));
}
sum(1, 10); // => 11It breaks down like this...
- We call
sum(1, 10). - Our
sumfunction ultimately returns a value. In this case whatever is returned by calling thetrampolinefunction. - The
trampolinefunction accepts a reference to a function as its argument (it's important to understand it needs a reference to a function; doingreturn trampoline(recur(x, y))wouldn't work as that would end up just passing the result of callingrecur(x, y)to thetrampolinefunction). - So we use
Function#bindto pass a reference to therecurfunction (we usenullas thethisbinding because it doesn't matter what the context the function executes in as we don't use the function as a constructor). - When we execute
sum(1, 10)we pass the reference to the internalrecurmethod to thetrampolinefunction. - The
trampolinefunction checks if we passed a function and if so we execute the function and store its return value inside thefvariable. - If what we pass into the
trampolinefunction isn't a function then we assume it's the end (i.e. accumulated) value and we just return the value straight back to thesumfunction which returns that value as the accumulated value. - Inside the
recurfunction we check to see ifyis greater than zero, and if it is we modify thexandyvalues (like we did in the previous example) and then return another reference to therecurfunction but this time with the modifiedxandyvalues. - Inside the
trampolinefunction the newly referenced function is assigned to thefvariable and thewhileloop on its next iteration checks to see iffis indeed a function or not. If it is (which it would be in this instance) we again execute the function (which is nowrecur(2, 9)) and the whole process starts again. - Until of course we reach the point where
yis set to zero. Then when thetrampolinefunction executes the function reference (recur) and inside theifconditional fails (asyis now zero and no longer greater than zero) and so we return the accumulatedxvalue; which then gets sent back and stored in thefvariable inside thetrampolinefunction. - On the next iteration of the
whileloop,fis now a value and not a function and so thewhileloop ends and the accumulated value is returned to thesumfunction which returns that as its accumulated value.
Awesome. 👍