Why Object-Oriented Languages Need Tail Calls
harukizaemon.com
harukizaemon.com
And TCO is so handy in so many instances that it's silly not to have it. And you gain performance.
You aren’t going to like Python:
I don't believe in recursion as the basis of all
programming. This is a fundamental belief of certain
computer scientists, especially those who love Scheme
and like to teach programming by starting with a "cons"
cell and recursion. But to me, seeing recursion as the
basis of everything else is just a nice theoretical
approach to fundamental mathematics (turtles all the
way down), not a day-to-day tool.
http://neopythonic.blogspot.com/2009/04/tail-recursion-elimi...(I’m not advocating this view, just pointing out that there are programmers who appear in all other respects to be reasonable, intelligent, contributing members of computing society who actually feel that TCO is a bug, that 1000 calls ought to be enough for anybody, and so forth.)
That said, there should be no reason not to have your new modern programming language support tail calls. It's short-sighted, and can negatively affect more than just the trivial recursive case.
So, new programming language inventors, if you're reading this, support tail call optimization and proper tail recursion from the beginning. Your language will be better for it.
There's many valid reasons not to support TCO. The first one that comes to mind: it makes generating stack traces more difficult.
Another reason: the JVM doesn't natively support TCO. This means to implement it you'll either need to use a trampoline (e.g. Erjang) or convert recursion to iteration (e.g. Clojure)
The latter isn't too difficult, but when tail recursion merely provides an alternate syntax for iteration, and mutual tail recall recursion isn't supported, is there really as much of a point if your language is something like Ruby and already provides multiple means of iteration?
> Another reason: the JVM doesn't natively support TCO. This means to implement it you'll either need to use a trampoline (e.g. Erjang) or convert recursion to iteration (e.g. Clojure)
So you avoid a 35 years old functionality that makes many procedures smaller, faster and more understandable just to make the compilers simpler?
Isn't this one of the usual tradeoffs where you (should) prefer having the compiler doing the boring and dirty job of converting a recursion to a loop, with all the bookkeeping of maintaining the state and making it thread-safe, simplifying the life of the application programmer at the expenses of the compiler programmer?
There are two extremes here, but there is also a huge middle ground, where TCO is implemented but recursion is not the basis for all computation. Some of my favorite compilers do that.
Recursive calls can blow the stack even with TCO....
I like TCO, but it lends itself to "infinite loops" that aren't as easy to figure out because they're not "local".
> And TCO is so handy in so many instances that it's silly not to have it.
"So handy" is even more true of lisp macros but ....
> And you gain performance.
It depends. If you're not doing a lot of interprocedural analysis, you can easily lose performance by writing "TCO-style".
let rec fact x =
if x <= 1 then 1
else x * fact (x-1);;
This implementation is not tail-recursive, so tail-call optimization will not help. Now, you can make the function tail-recursive by rewriting it to use a helper function with an accumulator: let fact x =
let rec f x a =
if x <= 1 then a
else f (x-1) * (x-a)
in
f x 1;;
This implementation is tail-recursive; however, this happens at the loss of readability. Furthermore, any such accumulator-based implementation can trivially be converted into a (non-recursive) loop, which is in fact exactly what the compiler will do.Now, if you look at the Hamster library code that inspired the post, you will see the following in hamster/list.rb:
def map(&block)
return self unless block_given?
Stream.new do
next self if empty?
Sequence.new(yield(head), tail.map(&block))
end
end
That's not tail-recursive, either, and the author would experience the self-same problems if ruby actually optimized tail calls. He'd have to rewrite his implementation for that.(If you look at OCaml's List.map, you will notice that it is not tail-recursive, either, and you will have to chain List.rev_map and List.rev to get a tail-recursive map.)
In short: tail-call optimization is no silver bullet that protects against stack overflow. It's not uncommon for programmers to assume that code is tail-recursive when it, in fact, isn't (which is one reason why Scala, for example, has the @tailrec annotation to let the programmer verify that a function is actually tail-recursive).
That does not mean that tail-call optimization is useless even in a procedural or object-oriented language. For example, if you have a language with formal function/procedure/method arguments that cannot be assigned to (that includes not only functional languages, but also Eiffel or Scala, and affects also the implicit self/this argument in any object-oriented language), then the only way to let the garbage collector free objects referenced by these arguments is to drop references to them via a tail call.
OO without TCO and lambda have a hole in the choices you have to encapsulate and abstract.
In a way, without TCO you must manually abstract each step and the next one and drive the stepping from outside. Not too unlike using an iterator instead of asking a collection to call f() for each entry.
Having read what he said I too feel that TCO is a very nice addition to other language features.
Too bad that the new project fortress blog at oracle.com is almost empty and filled with garbage (non-processed TeX sources).