4

So I recently came across case I needed to write code where callback calls itself and so on and wondered about NodeJS and tail-call support, so I found this answer https://stackoverflow.com/a/30369729 saying that yup, it's supported.

So I tried it with this simple code:

"use strict";
function fac(n){
    if(n==1){
        console.trace();
        return 1;
    }
    return n*fac(n-1);
}

fac(5);

Using Node 6.9.2 on Linux x64 and run it as node tailcall.js --harmony --harmony_tailcalls --use-strict and result was:

Trace
    at fac (/home/tailcall.js:4:11)
    at fac (/home/tailcall.js:7:11)
    at fac (/home/tailcall.js:7:11)
    at fac (/home/tailcall.js:7:11)
    at fac (/home/tailcall.js:7:11)
    at Object.<anonymous> (/home/tailcall.js:10:1)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)

Which clearly shows callstack gets filled with calls and tail-recursion isn't supported although I use latest NodeJS.

Does NodeJS/JavaScript support tail-recursion at all? Or do I really have to go with generators and yields, but problem here is my callbacks are gonna be heavily asynchronous and I'm not gonna work with return value anyway, I just need to make sure the callstack doesn't get uselessly filled with calls while function refers to itself in return.

Community
  • 1
  • 1
  • You could rewrite using a `while` loop instead of recursion. – jfriend00 Jan 15 '17 at 21:59
  • As I stated in question, I don't care about return result as callbacks are heavily async IO dependent, so using while is not possible –  Jan 15 '17 at 22:56
  • I'd suggest you show the actual example you're concerned about because having a function call itself from an async callback does not actually lead to stack buildup because the original function has already returned before the async callback gets called. So, we could likely help you better with your real problem if you showed your real problem. – jfriend00 Jan 15 '17 at 23:09

3 Answers3

8

What you have there is not a tail-call. A tail call is a function call performed as the final action of a another function. A tail-recursive call is the same except the function calls itself.

However, your code's final action is n*fac(n-1), not fac(n-1). This is not a recursive tail call because the current stack still needs to remember n while computing the recursive calls so it will know which numbers to multiply.

What you can do is compute this information 1 step before:

const fac = (n, result = 1) =>
  n === 1
    ? result
    : fac(n - 1, n * result);

console.log(fac(5)); // 120

Or in terms of your code:

function fac(n, result = 1) {
  // base case
  if (n === 1) {
    return result;
  }

  // compute the next result in the current stack frame
  const next = n * result;

  // propagate this result through tail-recursive calls
  return fac(n - 1, next);
}

Here's the stacktrace from Chrome Canary:

Proper Tail Calls in Chrome Canary

nem035
  • 34,790
  • 6
  • 87
  • 99
  • Well, but this is quite interesting, console.trace is able to print stack frames through current execution passed you said, it prints 1000 lines when I do fac(1000) using code provided by you. –  Jan 15 '17 at 22:53
  • And that means it has to be stored inside RAM somewhere taking useless space and if I made fac(1000000000), I would probably end up with at least 4GB RAM being used, which is very inefficient, correct me if I am wrong –  Jan 15 '17 at 22:54
  • Or is stack trace limited only to remembering last 10 steps? –  Jan 15 '17 at 23:00
  • Then what is use of tail-call optimisation in your example if I end up with stack trace being stored in RAM anyway taking place? Just purely theoretically, of course practically it will never reach over 20MB hopefully –  Jan 15 '17 at 23:02
  • 3
    To see if you actually have tail recursion optimization, call fac(10) and set a breakpoint inside the function and look at the stack trace when n has gotten down to 5 and see if you have stack buildup or not by looking at the current stack. – jfriend00 Jan 15 '17 at 23:05
  • Thanks, that's what I needed to know, that it's not entire stack frame –  Jan 15 '17 at 23:12
  • You should use [`"use strict";`](https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit/) at the beginning for a Proper Tail Call (PCT); yet even if so, when i check both your and my snippet at the Chrome debugger the call stack is growing. – Redu Jan 19 '17 at 16:58
  • @Redu The `use strict` might not be necessary, depending on [where](http://www.ecma-international.org/ecma-262/6.0/#sec-strict-mode-code) the code is executed. As far as the callstack growing, then either you're not using the exact code or your Chrome version doesn't have tail calls yet. Did you try [Canary](https://www.google.com/chrome/browser/canary.html)? – nem035 Jan 19 '17 at 19:09
  • @jakubinf I need to correct myself, `console.trace` will show you the proper call stack, which means that if you were seeing multiple stack frames then either your version of Node doesn't support PTC or you gave it a wrong flag. I attached an output from Chrome Canary. – nem035 Jan 19 '17 at 19:12
  • Well.. i as have understood for the [PCT (Proper Tail Calls)](https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit/) "use strict"; seems to be essential. Besides set aside my code (which is supposed to be TCO) i use your code and it's still growing up call stack. Basically this is why i was a little skeptical in my answer. Honestly i have never seen in ES6 TCO working the way it's supposed to be at all. Probably my oversight or something but i would like to know why. BTW my Chrome version is Linux V55.0.2883.87 (64-bit) just updated yesterday or something. – Redu Jan 19 '17 at 20:06
  • @Redu what I mean by it depends where the code is executed is that ES6 automatically enforces strict mode in certain places (the `where` in my previous comment links to the relevant spec portion). If you look at the added screenshot in my answer, you can see that PTC work in Chrome Canary. PTC are still a fairly new feature, which is why they are hidden behind flags and in canary builds. – nem035 Jan 19 '17 at 20:12
  • Hmm you might be right... but it seems there is still no Chrome Canary for Linux as of yet. I had assumed PCT was a flick of a switch after ES6. Apparently not so... – Redu Jan 19 '17 at 20:55
3

First off, if your actual case you're concerned about is when a function calls itself from an async callback, then you likely don't have any stack buildup there anyway.

That's because the original function has already returned and the whole stack unwound before the async callback gets called, so though it visually looks like recursion, there is no stack build up.

Here's a simple code example of making multiple sequenced network requests where a function calls itself from an async callback and there is no stack buildup.

function getPages(baseURL, startParam, endParam, progressCallback) {

    function run(param) {
         request(baseURL + param, function(err, response, body) {
             if (err) return progressCallback(err);
             ++param;
             if (param < endParam) {
                 progressCallback(null, body);
                 // run another iteration
                 // there is no stack buildup with this call
                 run(param);
             } else {
                 // indicate completion of all calls
                 progressCallback(null, null);
             }
         });
    }
    run(startParam);
}

The prior invocation of run() has already returned and the stack has completely unwound before the async callback is called so while this visually looks like recursion, there is no stack buildup.


In the specific code you show, you can avoid the recursion entirely by rewriting using a while loop which would work efficiently in any version of Javascript:

function fac(n){
    var total = 1;
    while (n > 1) {
        total *= n--;
    }
    return total;
}

// run demo in snippet
for (var i = 1; i <= 10; i++) {
    console.log(i, fac(i))
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Sometimes this is not possible when callback function is async IO dependent –  Jan 15 '17 at 22:55
  • @jakubinf - Well, it depends upon what exact code you're talking about. Repeatedly calling the same function from an async callback does not result in any stack buildup so there's no problem to solve there either. While it looks like a recursive call to the eye, the stack has already unwound and the original function returned before the function is called again so in terms of stack buildup, it's not really recursion. – jfriend00 Jan 15 '17 at 23:01
  • @jakubinf - I added more info to my question about the "async recursion" case. – jfriend00 Jan 15 '17 at 23:22
1

I am not sure your recursive function has a tail call. May be you can try the following;

"use strict";
var fac = (n, r = 1) => n === 1 ? r : (r *= n, fac(n-1,r));
console.log(fac(5));
Redu
  • 25,060
  • 6
  • 56
  • 76