Comment by eru
14 hours ago
Loops and gootos are just a special case of function calls, that needed to be invented because back in the olden days we had no clue how to design language and write compilers.
I don't understand why someone would want to hold on nostalgically to restrictions we no longer face.
Controlled mutability is a step up, yes. Btw, even languages like Haskell give you plenty of mutability to play with, if you want to. (Eg MVars and TVars are a thing.)
You are right that replacing a loop one for one with a tail recursive version doesn't give you much benefit either way. (Though it does make my life easier, because you don't have to contort my brain around keeping track of mutation.)
However that's a bit of a straw man and misses the broader picture.
What you'd want to do is define combinators that encapsulate the specific pattern of computation you want to implement. The most commonly known combinators are things like 'map' and 'filter'. But there are plenty more useful ones you can define, like various tree traversals or whatever makes sense for use cases and data structures.
Whether those combinators are implemented with tail calls or with loops or gotos or whatever is an implementation detail that their users don't need to worry about.
I know of one major use case where you'd want to use tail recursion directly: state machines and interpreters. See https://en.wikisource.org/wiki/Lambda:_The_Ultimate_GOTO for an example of the former.
> I don't understand why someone would want to hold on nostalgically to restrictions we no longer face.
On the other hand, I don't understand why someone would want to shoehorn all possible forms of control flow into "everything is a function object that can receive argument values that are substituted into parameter variables". I could just as easily say that all programs are a special case of cellular automata or string-rewriting systems or whatever other Turing tarpit.
You call imperative mutation "contorting your brain", but you always have to keep close track of which variables change and which stay the same, even if you shove them into function parameters. Local immutability comes at the cost of disguising which variables may have their values replaced over the course of the execution.
> What you'd want to do is define combinators that encapsulate the specific pattern of computation you want to implement.
Yes, I agree that control-flow combinators are a good model for many forms of programming. But they are mostly orthogonal to the broader imperative vs. functional style for sequential control flow, except for the historical factor of older imperative languages having poor support for capturing values into closures.
> I know of one major use case where you'd want to use tail recursion directly: state machines and interpreters. See https://en.wikisource.org/wiki/Lambda:_The_Ultimate_GOTO for an example of the former.
I can accept that direct threading makes contingent sense in that Python case, given current implementations. It seems to mostly be an unfortunate limitation that the compilers have poor handling of dispatch loops. (Indeed, the author still had to use special implementation-specific contortions to get the compiler to generate acceptable register handling.)
But otherwise, I'd generally prefer structured programming, whether it be with imperative statements or with control-flow combinators, over the totally unstructured control flow offered by bare tail calls.