Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Python's default is and should always be to be maximally helpful for debugging.

I can’t say I understand the whole topic but when someone who knows more than me says that…. It is a pretty compelling argument to me.



It's kind of a weird argument though. How do you expect a for loop to be represented in a stack trace?


The problem that’s hard to get around is this:

https://github.com/elixir-lang/elixir/issues/6357

Tail calls don’t have to be recursive. See also this old thread:

https://news.ycombinator.com/item?id=5376924


Good point, but surely there's a compromise somewhere. Keep the first TCO'd frame for reference maybe? Perfect stack traces aren't required.


Yeah, that could work in principle. However, if it's a language that’s using recursion for looping, then you'll loose that history every time you have a loop with more than n iterations (which could be quite often). Given that recursion can be indirect, you can't entirely eliminate that problem just by special casing direct recursion. It might still be better than nothing, though, I agree.


I would opt for balance, there is a reason why some languages compile in debug and release mode, because of the tradeoffs.

Having code that is optimal in performance often implies a tradeoff in debuggability. If debugging helpfulness is the major design decision of a programming language, that designer is trading off performance.


Tail calls fundamentally isn’t (just) about performance, it’s about language capability. Tail calls allow you to recurse indefinitely (because required stack space remains constant), which is not possible without tail calls (stack grows indefinitely). For example, tail calls make it okay to recurse on variable-length user input, which would be ill-advised in languages not supporting tail calls.


And not just direct recursion as is often considered. Mutual recursion is also handled neatly by this permitting you to write very clear state machines via functions and mutual recursion, if the tail calls get optimized. Very handy for parsing and similar tasks.


It certainly can be a trade-off. I think that's what they're saying for Python, maximizing helpful debugging > that feature.


So the whole topic is not terribly hard to understand. When you see

    return f(x', y', z')
in some function g, then g's stack frame just describes a forwarding proxy, “let me take the value returned by f and hand it to whoever called me.” And like with all forwarding proxies you can just delete the middleman and it works fine. You would do this because it gives you an alternate, debatably simpler, model for looping. In other loops you either have to return out midloop, or have to explicitly marshal your inputs and outputs of each step into mutable variables, see. So here is the same loop written two ways, the second is probably less familiar to you:

    function fib(n) {
      let curr=0, last=1;
      for (let i = 0, i < n; i++) {
        [ curr, last ] = [curr + last, curr];
      }
      return curr;
    }

    function fib(n, i=0, curr=0, last=1) {
      if (n == i) return curr;
      return fib(n, i + 1, curr + last, curr);
    }
These are only different styles for the same thing if you can trust that the call stack does not overflow in the second, which it doesn't have to because it returns a call to a function. So the problem is, if you have too many forwarding proxies in a chain, the language gives up on you.

Guido gives four reasons, you are quoting the first. The counterpoint there is, tail calls are just rewrites for looping constructs, as Guido admits in point 3. Should we ban loops as not “maximally helpful for debugging” because not every iteration appears on error stack frames? Perish the thought!

So at the end of the day that one just turns out to be, I am a lazy developer and don't want to figure out how to track this looping info in a way that makes sense outside of the call stack, the call stack exists and works, let's just keep it. And like, that's respectable!

The other 3 reasons are better? Reason 2 is correct, Python has multiple implementations and they'd all have to play, cf. the OP where JS implementations didn't. Reason 3 is correct but unimaginative, there's no reason you can't write a loop in this style and use Python's data structures, for that matter you can write in this style and not use any data structures, like the example above! Because the technical objection isn't really there, again, this boils down to just, Guido wants to read Python code and he finds recursion hard to read, and wants the language to make it deliberately slow so that he never has to read it. That one is valid, but it sounds almost borderline unethical? And I will admit that reason 4 fooled me at first! This appears to be a damning problem but in fact it's just smoke and mirrors, right? “I might not know who the forwarding proxy is forwarding in advance.” Yes that's true but we can agree that the forwarding proxy is unnecessary no matter what it is forwarding. Sure, your language sucks at referential transparency, but if you are conflating these two things you are confusing the issue, no?

Okay, so I started out this comment wanting to defend Guido and here I am at the end disagreeing with him...


> The counterpoint there is, tail calls are just rewrites for looping constructs, as Guido admits in point 3

Except they're not. Here are some functions using tail-calls:

    checks = {
      'odd' : lambda n: False if n <= 0 else checks['even'](n-1),
      'even': lambda n: True  if n <= 0 else checks['odd' ](n-1)
    }
These aren't a direct translation of loops, for a few reasons:

- Loops are statements, which can't be used in lambda expressions

- To use statements, we would need to define named functions (using `def`), but that too is a statement

- The names introduced by `def` would need to be unique, to avoid clobbering any existing names. This may require a fresh scope.

- We would need to inline each function's logic into the other

- We would need some intermediate state (separate from the argument 'n') to keep track of whether we're up to even or odd




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: