95

Functional languages lead to use of recursion to solve a lot of problems, and therefore many of them perform Tail Call Optimization (TCO). TCO causes calls to a function from another function (or itself, in which case this feature is also known as Tail Recursion Elimination, which is a subset of TCO), as the last step of that function, to not need a new stack frame, which decreases overhead and memory usage.

Ruby obviously has "borrowed" a number of concepts from functional languages (lambdas, functions like map and so forth, etc.), which makes me curious: Does Ruby perform tail call optimization?

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
Charlie Flowers
  • 17,338
  • 10
  • 71
  • 88

5 Answers5

133

No, Ruby doesn't perform TCO. However, it also doesn't not perform TCO.

The Ruby Language Specification doesn't say anything about TCO. It doesn't say you have to do it, but it also doesn't say you can't do it. You just can't rely on it.

This is unlike Scheme, where the Language Specification requires that all Implementations must perform TCO. But it is also unlike Python, where Guido van Rossum has made it very clear on multiple occasions (the last time just a couple of days ago) that Python Implementations should not perform TCO.

Yukihiro Matsumoto is sympathetic to TCO, he just doesn't want to force all Implementations to support it. Unfortunately, this means that you cannot rely on TCO, or if you do, your code will no longer be portable to other Ruby Implementations.

So, some Ruby Implementations perform TCO, but most don't. YARV, for example, supports TCO, although (for the moment) you have to explicitly uncomment a line in the source code and recompile the VM, to activate TCO – in future versions it is going to be on by default, after the implementation proves stable. The Parrot Virtual Machine supports TCO natively, therefore Cardinal could quite easily support it, too. The CLR has some support for TCO, which means that IronRuby and Ruby.NET could probably do it. Rubinius could probably do it, too.

But JRuby and XRuby don't support TCO, and they probably won't, unless the JVM itself gains support for TCO. The problem is this: if you want to have a fast implementation, and fast and seamless integration with Java, then you should be stack-compatible with Java and use the JVM's stack as much as possible. You can quite easily implement TCO with trampolines or explicit continuation-passing style, but then you are no longer using the JVM stack, which means that everytime you want to call into Java or call from Java into Ruby, you have to perform some kind of conversion, which is slow. So, XRuby and JRuby chose to go with speed and Java integration over TCO and continuations (which basically have the same problem).

This applies to all implementations of Ruby that want to tightly integrate with some host platform that doesn't support TCO natively. For example, I guess MacRuby is going to have the same problem.

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • 2
    I might be mistaken (please enlighten me if so), but I doubt TCO makes any sense in true OO languages, since the tail call must be able to reuse the caller stack frame. Since with late binding, it is not known at compile-time which method will get invoked by a message send, it seems difficult to ensure that (maybe with a type-feedback JIT, or by forcing all implementors of a message to use stack frames of the same size, or by restricting TCO to self-sends of the same message…). – Damien Pollet May 05 '09 at 13:36
  • 2
    That's a great response. That info is not easily found via Google. Interesting that yarv supports it. – Charlie Flowers May 05 '09 at 14:04
  • 15
    Damien, it turns out that TCO is actually *required* for true OO languages: see http://projectfortress.sun.com/Projects/Community/blog/ObjectOrientedTailRecursion. Don't worry too much about the stack frame stuff: it's perfectly possible to design stack frames sensibly so that they work well with TCO. – Tony Garnock-Jones Nov 23 '10 at 23:06
  • 2
    tonyg saved GLS' referenced post from extinction, mirroring it here: http://www.eighty-twenty.org/index.cgi/tech/oo-tail-calls-20111001.html – Frank Shearar Nov 26 '11 at 20:12
  • I'm doing a [homework assignment](https://www.edx.org/courses/BerkeleyX/CS169.1x/2013_Spring/courseware/Week_2x/Homework_1/) that requires me to disassemble a set of nested arrays of arbitrary depth. Obvious way to do it is recursively, and similar use cases online (that I can find) use recursion. My particular problem is very unlikely to blow up, even without TCO, but the fact that I can't write a completely general solution without switching to iteration bothers me. – Isaac Rabinovitch Jan 24 '13 at 21:43
  • I can't seem to find any disadvantage of TCO, why Python enforces _not to_ implement it? – karatedog Feb 12 '14 at 13:11
  • @karatedog http://stackoverflow.com/questions/13591970/does-python-optimize-tail-recursion tl;dr: author's opinion, weird tracebacks, undesired dependency on implementation details. Not that I agree with it, but these are the reasons. – D-side Dec 14 '14 at 15:58
  • You don't reuse the stack frame, you just remove the old one because you are done with it. – Gerry Gleason Jan 20 '15 at 20:04
  • Wow, just another reason why Matz is the nicest benevolent dictator ever. Man, you don't get nicer than that! – mydoghasworms Jan 29 '15 at 09:24
  • If anyone is interested in the article referred to by Frank and Damien, here's a new link that works. It's pretty interesting so I highly recommend it! http://www.eighty-twenty.org/2011/10/01/oo-tail-calls – Harfangk Dec 21 '16 at 10:25
44

Update: Here's nice explanation of TCO in Ruby: http://nithinbekal.com/posts/ruby-tco/

Update: You might want also check out the tco_method gem: http://blog.tdg5.com/introducing-the-tco_method-gem/

In Ruby MRI (1.9, 2.0 and 2.1) you can turn TCO on with:

RubyVM::InstructionSequence.compile_option = {
  :tailcall_optimization => true,
  :trace_instruction => false
}

There was a proposal to turn TCO on by default in Ruby 2.0. It also explains some issues that come with that: Tail call optimization: enable by default?.

Short excerpt from the link:

Generally, tail-recursion optimization includes another optimization technique - "call" to "jump" translation. In my opinion, it is difficult to apply this optimization because recognizing "recursion" is difficult in Ruby's world.

Next example. fact() method invocation in "else" clause is not a "tail call".

def fact(n) 
  if n < 2
    1 
 else
   n * fact(n-1) 
 end 
end

If you want to use tail-call optimization on fact() method, you need to change fact() method as follows (continuation passing style).

def fact(n, r) 
  if n < 2 
    r
  else
    fact(n-1, n*r)
  end
end
Ernest
  • 8,701
  • 5
  • 40
  • 51
12

It can have but is not guaranteed to:

https://bugs.ruby-lang.org/issues/1256

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • The link is dead as of now. – karatedog Feb 12 '14 at 13:12
  • @karatedog: thanks, updated. Although to be honest the reference is probably out of date, since the bug is now 5 years old and there's been activity on the same subject since. – Steve Jessop Feb 12 '14 at 15:03
  • Yes :-) I just read about the topic and I saw that in Ruby 2.0 it can enabled from source code (no more C source modification and recompile). – karatedog Feb 12 '14 at 21:35
4

TCO can also be compiled in by tweaking a couple variables in vm_opts.h before compiling: https://github.com/ruby/ruby/blob/trunk/vm_opts.h#L21

// vm_opts.h
#define OPT_TRACE_INSTRUCTION        0    // default 1
#define OPT_TAILCALL_OPTIMIZATION    1    // default 0
3

This builds on Jörg's and Ernest's answers. Basically it depends on implementation.

I couldn't get Ernest's answer to work on MRI, but it is doable. I found this example that works for MRI 1.9 to 2.1. This should print a very large number. If you don't set TCO option to true, you should get the "stack too deep" error.

source = <<-SOURCE
def fact n, acc = 1
  if n.zero?
    acc
  else
    fact n - 1, acc * n
  end
end

fact 10000
SOURCE

i_seq = RubyVM::InstructionSequence.new source, nil, nil, nil,
  tailcall_optimization: true, trace_instruction: false

#puts i_seq.disasm

begin
  value = i_seq.eval

  p value
rescue SystemStackError => e
  p e
end
Kelvin
  • 20,119
  • 3
  • 60
  • 68