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); // => 11
In 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) // => 100001
Here 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 thesum
variable. - The
sum
variable is now a function expression that when called (e.g.sum(1, 10)
) will execute theaccumulator
function thattco
returned. - The
accumulator
is a closure (meaning when we callsum
theaccumulator
will have access to the variables defined inside oftco
->value
,active
andaccumulated
; as well as our own code which is accessible via the identifierf
). - When we call
sum
for the first time (e.g.sum(1, 10)
) we indirectly executeaccumulator
. - The first thing we do inside of
accumulator
is push the arguments object (and Array-like object) into theaccumulated
Array (so theaccumulated
will now have a length of 1). - It's worth knowing that the
accumulated
Array only ever has 1 item inside of it (as we'll soon see). - The
active
variable by default isfalse
. So asaccumulator
is called for the first time, we end up inside theif
conditional, and then resetactive
totrue
. - Now we get to a
while
loop (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
while
loop simply checks whether theaccumulated
Array has any content. If it does then we call thef
function 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 theshift
Array method to actually remove it from theaccumulated
Array 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.length
will fail and so we would end up settingactive
tofalse
and ultimately return tosum
the value of the variablevalue
. - This is where it gets confusing, but hold tight!
- The
f
function is our own code. Our own code calls thesum
function (which indirectly calls theaccumulator
function).
The secret sauce to this entire chunk of code is coming up!
- If our code returns
x
then thewhile
loop 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 ify
is greater than zero, if it is then we returnx
, otherwise...) we callsum
again and pass through modified arguments. - This time when we call
sum
we don't get inside of theif
conditional becauseactive
is already set totrue
. - So the purpose of calling
sum
from inside our own code is simply to allow us to store the newly modified arguments inside theaccumulated
Array. - The
sum
function then returnsundefined
(as we weren't able to move into theif
conditional) - The flow of control then throws us back into the original
while
loop (that's still going, it hasn't stopped yet) andundefined
is what's stored into thevalue
variable. - At this point the
accumulated
Array has once again got some content and so thewhile
loop's condition passes once more. - And that is where the recursive magic happens. At some point our code is going to call
sum
with they
argument set to zero meaning that when theaccumulator
function calls our code the conditiony > 0
will fail and so we return the value ofx
(which has been incremented each time along the way). - When that happens,
x
is returned and assigned as the value to thevalue
variable, and so our code never calledsum
and thus theaccumulated
Array is never modified again; meaning thewhile
loop condition inside theaccumulator
function will fail and thus theaccumulator
function will end returning whatever value is stored inside thevalue
variable (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); // => 11
It breaks down like this...
- We call
sum(1, 10)
. - Our
sum
function ultimately returns a value. In this case whatever is returned by calling thetrampoline
function. - The
trampoline
function 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 thetrampoline
function). - So we use
Function#bind
to pass a reference to therecur
function (we usenull
as thethis
binding 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 internalrecur
method to thetrampoline
function. - The
trampoline
function checks if we passed a function and if so we execute the function and store its return value inside thef
variable. - If what we pass into the
trampoline
function isn't a function then we assume it's the end (i.e. accumulated) value and we just return the value straight back to thesum
function which returns that value as the accumulated value. - Inside the
recur
function we check to see ify
is greater than zero, and if it is we modify thex
andy
values (like we did in the previous example) and then return another reference to therecur
function but this time with the modifiedx
andy
values. - Inside the
trampoline
function the newly referenced function is assigned to thef
variable and thewhile
loop on its next iteration checks to see iff
is 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
y
is set to zero. Then when thetrampoline
function executes the function reference (recur
) and inside theif
conditional fails (asy
is now zero and no longer greater than zero) and so we return the accumulatedx
value; which then gets sent back and stored in thef
variable inside thetrampoline
function. - On the next iteration of the
while
loop,f
is now a value and not a function and so thewhile
loop ends and the accumulated value is returned to thesum
function which returns that as its accumulated value.
Awesome. 👍