-
-
Save Gozala/1697037 to your computer and use it in GitHub Desktop.
// Lack of tail call optimization in JS | |
var sum = function(x, y) { | |
return y > 0 ? sum(x + 1, y - 1) : | |
y < 0 ? sum(x - 1, y + 1) : | |
x | |
} | |
sum(20, 100000) // => RangeError: Maximum call stack size exceeded | |
// Using workaround | |
var sum = tco(function(x, y) { | |
return y > 0 ? sum(x + 1, y - 1) : | |
y < 0 ? sum(x - 1, y + 1) : | |
x | |
}) | |
sum(20, 100000) // => 100020 | |
function tco(f) { | |
/** | |
Takes `f` function and returns one that may be used for tail recursive algorithms. | |
**/ | |
var active = false, value, accumulated, args | |
return function accumulator() { | |
accumulated = arguments | |
if (!active) { | |
active = true | |
while (accumulated) { | |
args = accumulated | |
accumulated = null | |
value = f.apply(this, args) | |
} | |
active = false | |
return value | |
} | |
} | |
} |
@Raynos so the way it works is following:
- Every time
accumulator
(in our casesum
) is calledaccumulated
variable is set to passed arguments (see line #7) - If
accumulator
is notactive
(is in the process of tail recursion) then we enter if clause (see line #8). - If
accumulated
is set, reset it (by setting tonull
) and call wrappedf
with a accumulated arguments.
3.1.f
callsaccumulator
(in our casesum
) if giveny
is not0
.
3.2. This will perform 1. and will return quickly sinceactive
is true. - Return value of
f
is set tovalue
variable (which will be undefined unlessy
is0
) - Repeats loop 3 until
accumulator
isnull
(which will be only in case ifaccumulator
is not called from itself which is wheny
is0
). - Once finished looping deactivate
accumulator
and return last value returned byf
.
So trick is that on tail recursion will cause accumulated
to be set to a new parameters causing next iteration of the loop, this will continue until function returns with something other than tali recursion.
Also note that this function has side effects, and will break if foo = tco(function() { ... })
calls bar
that calls foo
. I don't think this is common case, but even if you have that case you can always call tco(foo)
from bar
instead of calling foo
. As far as I know tail call optimizations are on the table for ES.next so this is designed as temporary workaround until then.
@Gozala Awesome code!
You can simplify tco() to:
function tco(fn) {
var active, nextArgs;
return function() {
var result;
nextArgs = arguments;
if (!active) {
active = true;
while (nextArgs) {
result = fn.apply(this, [nextArgs, nextArgs = null][0]);
}
active = false;
}
return result;
};
}
// OR
function tco(fn) {
var queue;
return function() {
var args, result;
if (queue) {
queue.push(arguments);
} else {
queue = [arguments];
while ((args = queue.pop())) {
result = fn.apply(this, args);
}
queue = null;
}
return result;
};
}
Thank for suggestion @jdalton version with array indeed reads better so I revised implementation to use that.
result = fn.apply(this, [nextArgs, nextArgs = null][0]);
creates a gratuitous array literal. How about just putting nextArgs = null; on its own line?
/be
@BrendanEich current version build on @jdalton array suggestion but in slightly diff way avoiding that.
The reason that version uses an array is because nextArgs
needs to be falsey before fn.apply(...)
is executed and I didn't want to juggle another variable just for that purpose.
Putting nextArgs = null;
on its own line would either kill the args passed to fn.apply(...)
causing the loop to exit early
while (nextArgs) {
nextArgs = null;
result = fn.apply(this, nextArgs);
}
or null nextArgs
too late causing the loop to exit early as well
while (nextArgs) {
result = fn.apply(this, nextArgs);
nextArgs = null;
}
@Gozala's original version used the variable args
just for this juggle
while (nextArgs) {
args = nextArgs;
nextArgs = null;
result = fn.apply(this, args);
}
I created a jsPerf of the variations here as well as non-recursive alternatives:
http://jsperf.com/tco#chart=bar
I have made a module out of this as I intend to use it nodejs
https://github.com/Gozala/js-tail-call
Thanks for feedback and suggestions!
Slightly off-topic - I knew semicolons were mostly optional in JS but I did not know the code looks so much more readable without them! Something to learn for me coming from Java/C/C++:) Great stuff.
@ypocat Yeah I think so too. You might want to check out:
http://aresemicolonsnecessaryinjavascript.com/
Hurray for bad coding styles :P
@Gozala Thanks much for that link Irakli, the few gotchas are good to know.
@jdalton Well, coffee-script is the second most-depended-upon module in npm, but I will not use it because its local variable scoping is total trash (same as Ruby), the function parameter "shims" make for ambiguous APIs (same as Ruby), and besides the better code readability, it doesn't solve any real problems (e.g. async programming with proper error handling), to justify another compilation and indirection layer on top of JS. But seeing Gozala's code, I realized that the main reason why I was looking at CofeeScript was a slightly better readability, which JS without semicolons fully gives me. You could call it broken, but in my book, if there is a feature of the language which comes directly from the spec, and people don't use it despite the fact that it makes the code much more readable, I think it's actually their code that deserves the label "bad coding style", logically speaking.
@ypocat I meant we all prefer a certain code style and others stink :)
@Gozala. Thanks for your sharing. This is a little tricky at the first sight. I've got one question: Is this line active = false
at the accumulator()
really necessary?
I don't see how this would work. You have a
while (accumulated)
loop that will only run once asaccumulated
is set tonull
. Should that be anif
instead?