nzakas / understandinges6

Content for the ebook "Understanding ECMAScript 6"
5.45k stars 797 forks source link

About Tail Call Optimization #340

Open Bamieh opened 7 years ago

Bamieh commented 7 years ago

In your book, you have the following line:

The tail call does not require access to variables in the current stack frame (meaning the function is not a closure).

I was discussing the issue on stackoverflow, and a few pointed out that this is not in the spec, and closures do not interfere with TCO since free variables of closures are stored on the heap. So a closure can live longer than its surrounding scope, where it was declared.

Can you explain why closures would prevent TCO, is there any reason?

I looked in the spec, and indeed i was unable to find any mention to closures in regards to the TCO topic. section (12.4 i believe).

nzakas commented 7 years ago

Can you explain why closures would prevent TCO, is there any reason?

As far as I know, closures prevent TCO because the enclosing the stack frame is reused/overwritten for TCO. Closures rely on the enclosing stack frame for variable lookup, which means it can't safely be destroyed.

That said, none of this matters as it looks like implicit TCO will likely be removed in favor of explicit TCO.

getify commented 7 years ago

none of this matters as it looks like implicit TCO will likely be removed in favor of explicit TCO.

I think it definitely matters, because if closures prevent any sort of tail call technique, regardless of whether it's implicit or explicit, then this is definitely something that developers will have to be aware of. It's actually incredibly likely that functions people expect to be PTCd will have a closure over an outer scope, so this could be a huge caveat that would kill the usefulness of most PTC.

closures prevent TCO because the enclosing the stack frame is reused/overwritten for TCO

I don't think closure would be an issue in the general PTC case. Lemme illustrate:

var bar = (function IIFE(){
   function foo() { return 42; }

   return function bar(x) {
      if (x < foo()) return x;
      return bar( x / 2 );
   }
})();

bar( 608 );   // 38

Here, bar() definitely closes over foo, but I don't think that will prevent the PTC, because the closure is outside the scope of bar(). Each time the stack frame for bar() is recreated/reused, the closure it has over the enclosing scope of the IIFE() function remains intact and unchanged. Doesn't seem like there's any reason at all that the second call of bar() can't adopt/share the same scope closure that the first bar() call had. The outer scope doesn't ever change or go away while that recursion is going.

And now a counter example that definitely would be problematic for PTC and closure:

function bar(x) {
   if (x == 304) setTimeout( function inner() { console.log(x); }, 1000 );
   if (x < 42) return x;
   return bar( x / 2 );
}

bar( 608 );   // 38 

// 1 second later:
// 304

Here, the closure that inner() gets over the scope of bar() to remember the x, that definitely does seem like it would prevent that particular stack frame of bar() from being discarded.

The good news is, I suspect that closure inside of a recursive function is exceedingly rare, whereas closure outside of its own scope is probably pretty common.

Bamieh commented 7 years ago

@getify I agree that it is indeed rare to write inner closures like that in this context, explicit tail calls might make it clearer that tail calls are actually working, debugging implicit tail calls might not be the average programmer's thing i guess.

@nzakas so your statement holds true then, although outer closures should work fine since they are not affected at all, closures inside the recursive call prevent stack frame reseting in tail calls since closures rely on the enclosing stack frame for variable lookup, which means it can't safely be destroyed.