Hacker News new | ask | show | jobs
by thayne 1458 days ago
So, what happened to syntactic tail calls? I think that's what I would prefer anyway, both because it makes it more clear from a debugging standpoint, since you opt in, and because you can get a warning (or compiler/linter error if using a transpiler or linter) when your function isn't actually tail recursive.
2 comments

The whole "issue" is very strange to me. Proper tail calls (PTC) without the extra syntax are literally free performance boosts for existing code.

The whole "stack frames" argument is a red herring:

* Nobody expects stack frames to exist for every `for` loop which is the biggest practical use for PTC

* Stack frames go away the second you release control back to the event loop which is by far the more pernicious problem.

* Stack frames essentially just capture the continuation anyway. If my function `blah()` calls `foo()` and `bar()` before blowing up on `baz()`, neither of those functions will be captured by the stack frame which is no different than a CPS (continuous passing style) with PTC where you have `foo()` that returns `bar()` that returns `baz()` and `baz` throws. In BOTH cases, you'll see the stack frame for `baz`, a stack frame for `blah()` and frames for whatever called `blah()` up to the top of the stack or where the event loop made the stack frames disappear anyway.

* EDIT: I almost forgot to mention, but you can activate a "shadow stack" when the debugger is open (just like they already disable most optimizations when it's open) which can give you your reams of useless stack traces as your function executes a million times in a loop.

In short, programmers have performance to gain and not much of real value to lose by implementing PTC without syntax.

> The whole "issue" is very strange to me. Proper tail calls (PTC) without the extra syntax are literally free performance boosts for existing code.

It's bad to create a dangerous performance cliff. There could be some TCO based code that works fine, and then a junior coder makes an 'innocent' change that makes it ineligible for TCO, then that code eventually starts getting OOM crashes on heavy data.

I think if they're gonna do TCO then it really should be syntactic. If someone is writing their code around an assumption of TCO, then they almost always want an explicit guarantee of TCO, and they want to fail fast if TCO isn't happening.

This is simply untrue in practice.

Almost 52% of mobile web traffic in the US is iOS/Safari which implements proper tail calls. Despite this, we don't get constant stack overflows.

A little more than 1 in 9 use desktop Safari which also implements proper tail calls. We also don't see stack overflow issues here either.

It would be the other way around, you'd get the stack overflow issues in non-Safari browsers.

But because nowadays nobody is coding with TCO in mind, there are no such issues.

What paulhodge is pointing out is that once people start coding with the assumption of TCO, then issues crop up when someone changes a tail call into a non-tail call (or, a browser that doesn't implement TCO runs the program).

But right now, unless you are developing exclusively against safari, you can't depend on getting TCO. If all browsers implemented it, you could, and you get the performance cliff.

Maybe the performance gains are worth it, but the current situation doesn't really refute the claim that it could be a problem.

> Stack frames go away the second you release control back to the event loop which is by far the more pernicious problem.

Sadly the opposite is true today: If you're doing async/await programming in Chrome (and Firefox too, I think?) the runtime actually tries to carry your stack across event loop turns and this will be visible in Error.stack. This happens even with the debugger closed in my experience (the massive stacks are really annoying)

There are times where they are very useful.
Having had to debug issues in unfamiliar async-using code without the benefit of that feature (Now who the hell originally called this function?), I'd tend to agree.
I agree, syntactic tail calls seems like a great compromise if automatic tail calls are too risky. I'd love to have them in the language.

Here are some strawman syntax examples from the Syntactic Tail Calls proposal.

Return Continue

  function factorial(n, acc = 1) {
    if (n === 1) {
      return acc;
    }
    return continue factorial(n - 1, acc * n)
  }

  let factorial = (n, acc = 1) => continue
    n == 1 ? acc
           : factorial(n - 1, acc * n);

  // or, if continue is an expression form:
  let factorial = (n, acc = 1) =>
  n == 1 ? acc
         : continue factorial(n - 1, acc * n);
Function sigil

  // # sigil, though it's already 'claimed' by private state.
  #function() { /* all calls in tail position are tail calls */ }

  // Note that it's hard to decide how to readably sigil arrow functions.

  // This is probably most readable.
  () #=> expr
  // This is probably most in line with the non-arrow sigil.
  #() => expr

  // rec sigil similar to async functions
  rec function() { /* likewise */ }
  rec () => expr
!-return

  function () { !return expr }

  // It's a little tricky to do arrow functions in this method.
  // Obviously, we cannot push the ! into the expression, and even
  // function level sigils are pretty ugly.

  // Since ! already has a strong meaning, it's hard to read this as
  // a tail recursive function, rather than an expression.
  !() => expr

  // We could do like we did for # above, but it also reads strangely:
  () !=> expr
https://github.com/tc39/proposal-ptc-syntax#syntax-alternati...