Reputation: 4423
I'm not very experienced in JS. But as most people I sometimes have the need to add some extra functionality to a browser.
When looking for answers to other questions I came upon this answer at SO. The answer and the responder are highly upvoted, and by SO standards it means they are pretty reliable. What caught my attention is that in the "neatened up" variation it uses tail recursion to loop the function:
(function myLoop (i) {
setTimeout(function () {
alert('hello'); // your code here
if (--i) myLoop(i); // decrement i and call myLoop again if i > 0
}, 3000)
})(10);
From my perspective this looks like bad engineering. Using recursion to solve non recursive problems in an imperative/OO language is to ask for trouble. Ten or 100 iterations should be safe. But what about 10000 or an infinite loop? In purely functional languages as Erlang and Haskell I know that tail recursions are converted to loops during compile time and will not add an extra frame to the stack. That is as far as I know not the case for all compilers of for example C/C++ or Java.
How about JS? Is it safe to use tail recursion there without the risk of an SO? Or will this depend on the actual interpreter the script runs on?
Upvotes: 2
Views: 3984
Reputation: 43
Currently, tail recursion is not supported in most JS runtimes. Therefore unless you know exactly which runtime your code will be running, it will not be safe to rely on tail recursion to avoid "Maximum call stack size exceeded" error.
It is not supported in Node (with the exception of versions >6.4 & <8 where it can be enabled with a flag).
Safari versions 11 and 12 also appear to support it, but no other major browsers do.
Dr. Axel Rauschmayer has mentioned on his blog 2ality on 2018-05-09 that widespread support may never come.
Upvotes: 0
Reputation: 74204
The example you provided does not have any tail recursion. Consider:
(function loop(i) {
setTimeout(function main() {
alert("Hello World!");
if (i > 1) loop(i - 1);
}, 3000);
}(3));
main
and the outer function the name loop
.loop
function is immediately invoked with the value 3
.loop
function only does one thing. It invokes setTimeout
and then returns.setTimeout
is a tail call.main
is called by the JavaScript event loop after 3000
milliseconds.main
is called both loop
and setTimeout
have completed execution.main
function conditionally calls loop
with a decremented value of i
.main
calls loop
, it is a tail call.setTimeout
, the loop
function immediately returns. Hence, by the time main
is called loop
is no longer on the stack.A visual explanation:
|---------------+ loop (i = 3)
|---------------+ setTimeout (main, 3000)
|
|---------------+ setTimeout return
|---------------+ loop return
~
~ 3000 milliseconds
~
|---------------+ main (i = 3)
|---------------+ alert ("Hello World!")
|
|---------------+ alert return
| i > 1 === true
|---------------+ loop (i = 2)
|---------------+ setTimeout (main, 3000)
|
|---------------+ setTimeout return
|---------------+ loop return
|---------------+ main return
~
~ 3000 milliseconds
~
|---------------+ main (i = 2)
|---------------+ alert ("Hello World!")
|
|---------------+ alert return
| i > 1 === true
|---------------+ loop (i = 1)
|---------------+ setTimeout (main, 3000)
|
|---------------+ setTimeout return
|---------------+ loop return
|---------------+ main return
~
~ 3000 milliseconds
~
|---------------+ main (i = 1)
|---------------+ alert ("Hello World!")
|
|---------------+ alert return
| i > 1 === false
|---------------+ main return
Here's what's happening:
loop(3)
is called and 3000
milliseconds after it returns main
is called.main
function calls loop(2)
and 3000
milliseconds after it returns main
is called again.main
function calls loop(1)
and 3000
milliseconds after it returns main
is called again.Hence, the stack size never grows indefinitely because of setTimeout
.
Read the following question and answer for more details:
What's the difference between a continuation and a callback?
Hope that helps.
P.S. Tail call optimization will be coming to JavaScript in ECMAScript 6 (Harmony) and it's perhaps the most awaited feature on the list.
Upvotes: 10
Reputation: 214949
That code is not recursive per se, quite the contrary, it uses continuation passing to eliminate tail calls. Here's an example without setTimeout
:
// naive, direct recursion
function sum_naive(n) {
return n == 0 ? 0 : n + sum_naive(n-1);
}
try {
sum_naive(50000)
} catch(e) {
document.write(e + "<br>")
}
// use CPS to optimize tail recursive calls
function sum_smart(n) {
function f(s, n) {
return n == 0 ? s : function() { return f(s+n, n-1) };
}
var p = f(0, n)
while(typeof p == "function")
p = p()
return p;
}
document.write(sum_smart(50000) + "<br>")
CPS is commonly used for tail recursion optimization in languages that don't support it out of the box. Javascript's setTimeout
basically takes the current continuation and "throws" it to the main thread. Once the main thread is ready, it "catches" the continuation and runs the code in the restored context.
Upvotes: 5
Reputation: 8715
This is not a clear recursion. Each call of myLoop
will be performed on another stack of execution (somewhat like a separate thread) and does not rely on previous calls. As in original answer:
The setTimeout() function is non-blocking and will return immediately.
There is a myLoop
function, that launches a timeout and an anonymous function, that handles, what should be performed after that timeout. The value, returned by myLoop()
(which will be undefined
) is not used later in the calls.
Upvotes: 1