
Why Object-Oriented Languages Need Tail Calls (2011) - tonyg
https://eighty-twenty.org/2011/10/01/oo-tail-calls
======
rbehrends
There are a number of problems with this argument, but the biggest one is that
most such calls will not actually be tail-recursive. The very specific
`adjoin` example that has been provided only works with `contains` not being
in a tail position because `or` is assumed to use short-circuit evaluation,
and because the author is apparently willing to live with a set membership
test that has linear time complexity. We can see that the `contains` method
for `UnionObject` is not tail-recursive, for example, no matter how you cut
it.

The primes example effectively constructs a linked list with 78,498 elements;
any unsuccessful search will iterate over all those elements, and successful
searches will (depending on how the search arguments are distributed) also
easily traverse tens of thousands of elements. Handwaving performance issues
away with "we know lots of ways this code can be optimized, but let us please
focus on the behavior of the constructed set" is wrong, simply because
efficient and actually usable implementations may exhibit totally different
requirements w.r.t. tail recursion and what should be in an interface.

~~~
phkahler
"is wrong, simply because efficient and actually usable implementations may
exhibit totally different requirements w.r.t. tail recursion and what should
be in an interface."

Thank you so much for that!

------
mikeash
Why don't languages give tail calls their own syntax rather than making it an
optimization? It seems like a lot of the arguments against tail calls boil
down either 1) it's confusing when things go wrong and you weren't expecting
stack frames to be optimized out or 2) it's hard to safely write tail-
recursive code because a small change to the code or, worse, the compiler
settings can make it stop being tail-recursive.

If you had to explicitly request it, say by doing `tailcall f()` instead of
just `f()`, that would mostly fix these problems. Control flow will be a lot
less confusing since it'll be explicit in the code (but you still lose
information from the optimized-out stack frames, no way around that) and
you'll get an explicit error if someone tries to add something that can no
longer be tail call optimized, like `tailcall f() + 1`.

~~~
ajross
> Why don't languages give tail calls their own syntax rather than making it
> an optimization?

Grumpy old man answer: because if you need "special syntax" to express the
fact that your recursively defined function can _technically_ be evaluated in
constant stack space, you probably should be writing it as an iterative
function to begin with.

The whole point about recursion as a software engineering paradigm is that
it's a _clearer_ way to express the problem and thus less likely to be
mistakenly implemented. Special syntax is the opposite of clear.

Iteration ain't that hard either folks, and it has the notable advantage of
mapping more obviously to the behavior of actual machines we use to execute
our code.

~~~
mikebenfield
> because if you need "special syntax" to express the fact that your
> recursively defined function can technically be evaluated in constant stack
> space, you probably should be writing it as an iterative function to begin
> with.

Why? I find recursive functions are often simpler to understand and write than
iteration. Why does the compiler need to stop me from doing so? You write
"technically" as though it were just an obscure technicality that it's
possible to implement a function call without taking up excessive stack space,
but to my mind it's a fundamental fact.

> Iteration ain't that hard either folks, and it has the notable advantage of
> mapping more obviously to the behavior of actual machines we use to execute
> our code.

In what sense is this true? Recursive function: update these variables (in
registers or memory) and jump back up to this instruction. Iteration: update
this variable and jump back up to this instruction.

~~~
Spivak
It is an obscure technicality: your code only works because a compiler
optimization makes it work. If a different compiler executed your code exactly
as written it would crash. Or another way, two pieces of code that are
logically equivalent will succeed or crash depending on whether it's written
in such a way that the compiler can recognize an optimization.

I'm in favor of having special syntax because compilers shouldn't really be in
the business of 'do what I mean not what I say'.

~~~
nybble41
Despite the name "tail call optimization", the proper implementation of tail
calls is not "just" an optimization. Preserving a stack frame for a function
which has nothing left to do is incorrect behavior on the part of the compiler
which negatively impacts both the time and space complexity of the resulting
object code. The code, as written, does not require a stack frame. If the
compiled object code crashes it is not because it was executed "exactly as
written", but because the compiler inserted something beyond what was written.

The Scheme approach of mandating the use of tail calls for every call which
occurs in "tail position" is the correct one, in my opinion.

~~~
Spivak
> nothing left to do is incorrect behavior on the part of the compiler

But it's not though. By this token failing to optimize anything could be
considered incorrect behavior. Obviously not turning on optimizations makes
your program take longer and use more memory. When a function call is made its
variables are pushed onto the stack and popped when the function returns.
Doing something different when the compiler detects certain properties of your
code is the definition of an optimization that at least in theory shouldn't be
relied on to produce correct code.

A more correct design, I think, would be having a 'replace' keyword for
function calls instead of return that causes it to replace the stack of the
caller. Then the behavior is no longer an optimization but behavior. Now you
have the best of both worlds. The compiler can even warn you about when you
might want to use replace rather than return.

~~~
tweakism
You suggest literally the same solution that the person you were arguing with
suggested!

------
cryptonector
One would think that by now tail call optimization (TCO) would be understood
to be absolutely necessary. TFA makes a very good case, but it's hardly the
first or only example of someone making a convincing argument for TCO.

Even in jq we found that adding TCO enabled a number of interesting
possibilities. For example, the range() builtin in jq can be implemented as a
tail-recursive function (and the version that takes two or three arguments
is). There are a number of builtins in jq 1.5 which are made possible and
practical only by having TCO. Before we added TCO it just didn't seem that
interesting, but it's proven to be quite an enabler.

I can't think of a language that shouldn't have TCO. C/C++ absolutely should
have it (and many compilers do). Java should have it (but doesn't). Lisps
badly need it and generally have it -- the same goes for all functional (or
mostly functional) languages. Python needs it but apparently lacks it[0]. And
so on.

Yes, tail calls obscure stack traces by eliding reused frames. This could be
ameliorated by leaving a flag in reused frames that could be shown on stack
traces to indicate that there are missing frames. Or perhaps syntactic sugar
could be used to control which tail calls are to get optimized (since often it
does not matter).

[0] [https://stackoverflow.com/questions/13591970/does-python-
opt...](https://stackoverflow.com/questions/13591970/does-python-optimize-
tail-recursion/13592002#13592002)

------
btilly
This is an old debate.

On the one hand with tail calls you can write recursive code and find that it
runs fast without excessive memory.

On the other hand when things blow up, it is really nice to have a stack
backtrace to help debug _why_ it blew up. But a stack backtrace can't include
stack frames that were optimized away. Which makes it far harder to figure out
why this call on this object turned into that call on that object.

~~~
steego
> On the other hand when things blow up, it is really nice to have a stack
> backtrace to help debug why it blew up. But a stack backtrace can't include
> stack frames that were optimized away. Which makes it far harder to figure
> out why this call on this object turned into that call on that object.

Is that really a concern?

If you're writing a loop, do you expect your tools to show you a trace of
every iteration in the loop? Of course not, because that would be silly. If
you really want to trace each iteration, you can add your own tracing.

I think of tail-recursion as parameterized gotos that are wonderfully
constrained and are useful for describing state transformations in a
predictable way. It's not a regular function call, so the stack trace is
probably a very bad idea.

~~~
sbmassey
Tail recursion optimisation gets rid of _all_ tail calls in functions, not
just those that might lead to loops - you cannot in general detect those calls
that might be loops. This can put a lot of holes in your stack trace.

~~~
willtim
It's an optimisation. It can be turned off when debugging with stack traces.
If you need stack traces in production code, you have too many partial
functions.

~~~
Rusky
But you can't ever turn it off if you rely on it for loops, which defeats the
whole point.

~~~
willtim
You can turn it off, you just can't process large data sets, as you'd run out
of stack space. But who wants to debug with large data sets?

~~~
stormbrew
People who are debugging behaviour with large data sets perhaps? :P

~~~
willtim
The runtime could allocate "stack frames" on the heap, for such cases.

------
pmontra
Ruby's got TCO since 1.9 but it must be enabled at runtime in a pretty ugly
way [1]. I'm also working on Elixir which has TCO and it's the basis for
running server processes: the server function calls itself passing the server
state as argument. I don't see why OO languages shouldn't do the same. The
argument about losing the stack trace is pretty weak. Nobody wants a million
stack frames to debug. If the program dies we check the last values of the
variables and that's it.

[1] [http://rpanachi.com/2016/05/30/ruby-recursion-stack-size-
tai...](http://rpanachi.com/2016/05/30/ruby-recursion-stack-size-tail-call-
optimization)

~~~
rurban
Yes. The same argument can be made against fork(), that fork makes debugging
too hard. And we don't know if the value we want to inspect is a common one
(before cow) or an already copied. So do we forbid fork()?

------
goialoq
It's [2009], not [2011]. 2011 is when the current host copied the document
from the archive.

------
phkahler
I'm not a fan of this style of coding. It's not just the stack space. When you
call methods all over the place it can result in inefficient code. And yes, I
understand that I wrote "can" in that sentence. We're talking about a case
where the author wants the compiler to be even smarter than it already is. I
guess that's my point. Using these kinds of abstractions results in a reliance
on the compiler to make things efficient. OTOH using abstractions can make
things simpler and more understandable for the programmer. But there's that
word "can" again.

------
jacinabox
They don't need tail calls, unless you're planning on doing idiomatic
functional programming in an OO language, which, Why??

~~~
samth
The answer to your question is the entire point of the blog post, which was
written by Guy Steele, who worked on the original design of Java.

