News for hackers. Stuff that matters.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @12:32AM (#25681831)
This is actually (in the abstract) exactly how I’ve read and understood it in FP, with the necessary added note that when you TCO without recursion you lose history for eg stack traces and debuggers.

It’s also astonishing to me that people in a position to explain even moderately complex computing ideas continue to treat recursion as somehow hard to reason about. It’s calling a function. A function that calls itself is still just a function calling another function. Two mutually recursive functions calling each other are just two functions calling two other functions. That’s so much easier to follow than state changing in a single function call.

Re:How Tail Call Optimization Works by FeepingCreature on Friday January 08, 2021 @01:25AM (#25682117)
> It’s also astonishing to me that people in a position to explain even moderately complex computing ideas continue to treat recursion as somehow hard to reason about. It’s calling a function.

Calling a function can be easily, and incorrectly, modeled by representing the arguments as variables stored at a fixed position in memory. (As function calls used to work.) Once you introduce recursion, this model breaks down and you are forced to think in terms of activation records/stackframes.

Probably those people learnt function calls in the former way first, meaning that recursion represented a conceptual leap for them.

Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @02:26AM (#25682446)
Maybe it’s more of me showing my naivety but I still think of a recursive function call as bindings and computation. My mental model is f(x) -> f(y(x)) = f(x) -> g(x), where g is a function wrapping the body of f. It’s surely not what the compiler does, but it makes thinking about recursion no more complicated than thinking about any old function. Then the only thing I need to think about beyond that is “does my environment support TCO, or am I certain my data fits in the call stack?”
Re:How Tail Call Optimization Works by manwe150 on Friday January 08, 2021 @02:39AM (#25682514)
Consider that some processors were too small to fit a stack. Some languages (notably BASIC), only had what you might call 'static' variables (to use the C name), so when you recursed into them, the variables would be shared across all calls to that function.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @02:45AM (#25682548)
And that’s a good case for supporting recursion but eliminating branching! You can goto and rebind to your heart’s content as long as you don’t have to back out of a condition
Re:How Tail Call Optimization Works by mcguire on Friday January 08, 2021 @12:32PM (#25687200)
Or expressions.
Re:How Tail Call Optimization Works by avhon1 on Friday January 08, 2021 @04:02AM (#25682911)
IMO, the two challenging parts of recursion are:

First, realizing that you can have separate instances of functions at the same time -- different stack frames. Making the mental leap from "the content of x in function f" to "the content of x in f(0), and also the content of x in f(1)" is a leap of abstraction, but not necessarily a tremendous one.

Second, mentally modeling recursion when it gets complicated. Simple recursive cases are easy to model in your head, because either there is little state to keep track of, or because you can extrapolate the pattern between function calls, mentally compressing the state of the whole call tree.

Tail-call optimization, for example, is really easy to mentally model because you only have to keep track of the state of one function's variables at a time -- unless you start passing continuations or thunks into your function calls, in which case you're carrying a stack of unlimited complexity into each function call. It's still just a function call, with the same number and types of arguments, but now one or more of those arguments contains turing-complete, recursive programs of unlimited complexity.

This similar to the worst-case mentally-complicated recursion without TCO, where you can build a complex graph of function calls, scopes, shared data accesses, and mutated copies of what used to be the same data, that can all be present at once, and where you might need to keep track of all of them in order to track down and understand something that you need to understand.

You can, of course, build similarly tangled-up structures in a purely-iterative manner, but the upside is that the complexity will be confined to the data state -- there will be no call tree to mentally model, and no need to track variable scope.

---

Shorter version: if the efficiency and elegance of recursion (with appropriate compiler/runtime optimizations, like TCO) is like zip compression for your mental model of code complexity, then there exist recursive-program equivalents of zip bombs.

---

Even shorter version: recursion itself isn't hard to reason about, but recursive programs can be very hard to reason about

Re:How Tail Call Optimization Works by Geminidog on Friday January 08, 2021 @03:32AM (#25682768)
>It’s also astonishing to me that people in a position to explain even moderately complex computing ideas continue to treat recursion as somehow hard to reason about.

This is because you don't understand humans. Which is more clear to you?

    repeat special-task 5 times. 

    (do special-task n times) = (do special-task n - 1 times)
    (do special-task 0 times) = Don't do anything. 
    do special-task 5 times.
Clearly when we communicate with others using regular language we use a repeat keyword rather then recursive grammar. No natural language on the face of the earth ever communicates the concept of repetition via recursion* There's always some keyword or number for repeating something.

*(not 100% sure on this, HNers, prove me wrong if possible, I'm open to being wrong).

Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @11:05AM (#25686026)
> This is because you don't understand humans.

I don’t think it’s about so large and broad a group, or about human language. No one speaks in dynamic property access or dependency inversion or binary compilation or a zillion other concepts in programming that aren’t misrepresented as “hard to understand”.

My point isn’t that it’s something that would be immediately obvious to a beginner, but that its difficulty is often overstated when introduced.

Re:How Tail Call Optimization Works by Geminidog on Friday January 08, 2021 @12:43PM (#25687356)
Fair point. My point is that looping should be immediately obvious to a beginner due to a 1 to 1 mapping with human communication. Thus recursion when compared to iteration is way harder.
Re:How Tail Call Optimization Works by lmm on Friday January 08, 2021 @04:51AM (#25683149)
I don't think anyone's opposed to having a function for "repeat this thing 5 times". But if you did see a function that called itself, why would that be any more confusing than seeing a function that calls another function? That's all it is.
Re:How Tail Call Optimization Works by rcxdude on Friday January 08, 2021 @06:21AM (#25683551)
I think it's because for beginners it's hard to see why it would be useful (or not just turn into infinite regression). Proof by induction is often similarly difficult for maths students to grasp. The idea of solving a problem by breaking it down into smaller problems until each part of it becomes trivial is not something which comes naturally to most people, it's a learned and ingrained habit in those who have been taught it and used it.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @11:11AM (#25686101)
> I think it's because for beginners it's hard to see why it would be useful (or not just turn into infinite regression).

And that, in turn, I think is because most beginners are first introduced to mutation and statements and all kinds of “do” stuff instead of trivial algebra with trivial types.

If the core primitives at the start are:

1. Computation producing output

2. Computation of input producing output

3. Wrapping #2 in a function

I could see introducing recursion in an easily understandable way on day one.

Re:How Tail Call Optimization Works by Geminidog on Friday January 08, 2021 @12:45PM (#25687388)
Only in hindsight. In reality looping is much more easily grasped due to the one to one mapping with human language. Looping should be taught on day 1, not recursion.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @01:40PM (#25688148)
In hindsight lots of unfamiliar concepts are easy. Teaching programming with imperative constructs first encourages imperative thinking. Teaching programming with computational concepts encourages computational thinking. Both require some reasoning about syntax and control flow before they sink in enough for beginners to use them effectively. But I really don’t think human language has any bearing here. People are also very likely to already understand expressions, as they’re taught from a very early age in arithmetic and it’s very common for people to at least have some algebra education. Expressions are fundamentally easier to reason about, and there’s no reason they would be more difficult to a beginner.

I’d even say that people usually have a good grasp on recursion even if they don’t know that’s what it’s called. Multiplication is just recursive addition. Exponents are just recursive multiplication. And so on.

Re:How Tail Call Optimization Works by Geminidog on Friday January 08, 2021 @01:58PM (#25688540)
>Teaching programming with imperative constructs first encourages imperative thinking. Teaching programming with computational concepts encourages computational thinking

Imperative thinking is computational. All forms of thinking are computational by definition.

> Expressions are fundamentally easier to reason about, and there’s no reason they would be more difficult to a beginner.

What does expressions have to do with anything. Recursion can be both defined in an expression and as an imperative jump instruction. The concept is orthogonal to expressions.

>I’d even say that people usually have a good grasp on recursion even if they don’t know that’s what it’s called. Multiplication is just recursive addition. Exponents are just recursive multiplication. And so on.

But people don't think of multiplication this way. The teacher doesn't teach multiplication to you in terms of recursion she literally teaches you it with the "times" keyword. What's 5 * 6? Add 5 to 5, 6 times.

It has bearing on language. Nobody outside of programming/mathematics communicates concepts recursively. Our communication is a reflection of how we think naturally. Recursion takes training. Looping is just learning syntax for a concept we already know about: repetition.

Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @03:19PM (#25689875)
> Imperative thinking is computational. All forms of thinking are computational by definition.

I meant in the mathematical sense. Math doesn’t have imperative statements and side effects.

> What does expressions have to do with anything. Recursion can be both defined in an expression and as an imperative jump instruction. The concept is orthogonal to expressions.

In case it wasn’t clear, I’ve been arguing this whole time for teaching pure FP concepts. Starting with computing a value, then computing a value from input, then with a function returning a value computed from its input... then with a function computing a value recursively from its input. Recursion is the fundamental building block for repetition in FP. Even if you can express it with a loop-like expression it eventually desugars to recursion.

> But people don't think of multiplication this way. The teacher doesn't teach multiplication to you in terms of recursion she literally teaches you it with the "times" keyword. What's 5 * 6? Add 5 to 5, 6 times.

That... is recursion. It’s mind boggling to me that it’s not plain as day, here in this context. It’s literally:

    (+ 5 (+ 5 (+ 5 (+ 5 (+ 5 5))))))
And that’s precisely how I was taught multiplication. It was just written with infix:

    5 * 6 = 5 + 5 + 5 + 5 + 5 + 5
Which, the teacher demonstrated can be partially unwrapped:

    5 * 6 = 5 + 5 * 5
And so on. All you have to do to teach that as recursion is declare those operators as functions and call them. You can also use this to teach reduce, which is just a generalized recursive loop expression.

> It has bearing on language. Nobody outside of programming/mathematics communicates concepts recursively. Our communication is a reflection of how we think naturally. Recursion takes training. Looping is just learning syntax for a concept we already know about: repetition.

From a learning perspective, they’re literally just different syntax for the same thing. Recursion is just moving the repetition intent to the body of the loop and eliminating intermediary values. There’s no reason other than tautology that one syntax is easier to learn than the other.

Re:How Tail Call Optimization Works by Geminidog on Monday January 11, 2021 @11:42AM (#25731447)
In math iteration and recursion are defined as sibling isomorphic concepts. You can desugar recursion into iteration and you can “desugar” iteration into recursion. See Turing completeness and lambda calculus. The topic at hand is which angle provides a better view of this singular isomorphic concept (iteration vs. recursion) that makes it easier to understand. The isomorphism itself (which is what your expose gets into) is already well known.
Re:How Tail Call Optimization Works by Geminidog on Friday January 08, 2021 @12:40PM (#25687327)
Look at my example. if you literally communicated with someone that way would the other person be confused? The answer is yes.
Re:How Tail Call Optimization Works by lmm on Monday January 11, 2021 @07:18PM (#25740399)
You're not comparing like with like - you're comparing a repeat function (or keyword) with the implementation of a repeat function. The imperative equivalent would be something like "make an int called i, then repeatedly add one to i, checking whether i is less than five, and each time you do that do the special task", which is by no means clearer. And if anything writing a "repeat" function is easier in the functional world than the imperative one, since traditional imperative languages don't have first-class functions.
Re:How Tail Call Optimization Works by brundolf on Friday January 08, 2021 @01:33AM (#25682163)
The human intuition starts out naturally oriented around cause and effect, linear timelines of events, and state change over time, because that's how we've evolved to conceptualize the physical world around us. When you're designing large systems this model doesn't scale, but it's absolutely the most intuitive on the small-scale. This is why languages like Python and Basic are so accessible for newcomers.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @02:40AM (#25682521)
I’d say that you can model conceptually the same way with a time traveling debugger and get really comfortable. A recusing function is just an event loop where you know how the data is being mutated.
Re:How Tail Call Optimization Works by bhawks on Friday January 08, 2021 @01:25AM (#25682115)
I've seen people hung up on the calling yourself while defining yourself concept. Talking through that often highlighted a mental model of functions where they were sort of almost cut'n'pasting the other function into their code. Basically their model was too low level and the understanding of defining and calling functions were unnecessarily conjoined.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @02:37AM (#25682499)
I guess the way I’d try to help those people understand it is with the fiction I tell myself (which I alluded to upthread): I mentally model the “outer” function and the “inner” recursion as referencing different functions that have the same signature and the same body. The definition is concrete and self sufficient, it just dependency-injects a contract in its own image to delegate future work to. It’s even easy to literally implement that way in languages with first class functions, because you can just bind fn a to b and call b within a.
Re:How Tail Call Optimization Works by Scarbutt on Friday January 08, 2021 @01:33AM (#25682166)
you lose history for eg stack traces and debuggers.

I remember javascriptcore solving this issue by using a "shadow stack"

Re:How Tail Call Optimization Works by kevin_thibedeau on Friday January 08, 2021 @12:35AM (#25681839)
A better mind bender is an async promise calling itself.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @02:12AM (#25682371)
Wait, I’m confused. Like one promise/future/whatever monadish-eventually-fulfilled value referencing its own value? Or a polymorphic promise/etc referencing the same construct that created it to map its resolution into a new value?
Re:How Tail Call Optimization Works by aste-risk on Friday January 08, 2021 @01:42AM (#25682214)
how do modern compilers like clang/gcc handle the debug info while still doing TCO? Any description or links appreciated.
Re:How Tail Call Optimization Works by tomp on Friday January 08, 2021 @09:22AM (#25684961)
Clang will very aggressively optimize with -O2 (or whatever the max speed optimization is). It think gcc and MSVC as well.

I was experimenting with a simple indirect-threaded interpreter, which I wanted to write in a "nice" style (1 function per opcode) but have compiled in an "optimized" way (state machine). Playing around in Godbolt, I found out that the compilers managed to detect & optimize even indirect tail calls, i.e. tail calls to function pointers. This was indeed on a toy example (i.e. like 5 functions, not a real interpreter with 100 different opcodes) but still truly amazing.

The reason for this was, I was trying to replicate LuaJIT2 - Mike Pall was complaining that the reason interpreters written in C are slow is, that the compiler cannot optimize & register-allocate a large function containing a switch statement for 100 opcodes. Instead, he wrote the interpreter in asm, and made sure all important variables are kept in registers. My thesis was that we could do the same thing by having a bunch of functions tail-calling each other, with all important variables as parameters (which would be optimized into registers). Better, actually, because the compiler could improve register allocation within each "opcode"/function.

Re:How Tail Call Optimization Works by millstone on Friday January 08, 2021 @02:23AM (#25682428)
They don't. The whole point of TCO is to discard stack frames, which means local variables cannot be recovered. Optimize or debug, pick one.
Re:How Tail Call Optimization Works by manwe150 on Friday January 08, 2021 @02:44AM (#25682541)
My recollection (without references) is that they can do some amount of guess work. They might see the stack says A->C, but can see the code of A actually calls B, so logically it must have been A->B->C at some point.

Or you can use the rr-debugger, and just return back to the exact sequence of calls, and reject the need to pick between optimization and debugging :D

Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @02:42AM (#25682529)
Well, the point of TCO is to discard stack pressure. Discarding stack frames is just an unfortunate side effect.
Re:How Tail Call Optimization Works by Geminidog on Friday January 08, 2021 @03:36AM (#25682797)
clang will actually optimize non tail recursive function as well. Check out the code in this question. It will not stack overflow.

https://stackoverflow.com/questions/65225761/trying-to-delib...

Re:How Tail Call Optimization Works by namibj on Friday January 08, 2021 @06:55AM (#25683728)
Pun intended?
Re:How Tail Call Optimization Works by Geminidog on Friday January 08, 2021 @12:36PM (#25687267)
People who say "no pun intended" are cowards. Intend your puns weaklings.
Re:How Tail Call Optimization Works by eyelidlessness on Friday January 08, 2021 @01:43PM (#25688203)
I usually say “punexpected” because I think what’s meant by “no pun intended” is usually “I only recognized the pun after I said it”. But I agree! Puntention is best.