Simplifies compiling code with tail call optimization in MRI Ruby
MIT License
The tco_method
gem provides a number of different APIs to facilitate
evaluating code with tail call optimization enabled in MRI Ruby.
The TCOMethod.with_tco
method is perhaps the simplest means of evaluating code
with tail call optimization enabled. TCOMethod.with_tco
takes a block and
compiles all code in that block with tail call optimization enabled.
The TCOMethod::Mixin
module extends Classes and Modules with helper methods
(kind of like method annotations) to facilitate compiling some types of methods
with tail call optimization enabled.
The TCOMethod.tco_eval
method provides a direct means to evaluate code strings
with tail call optimization enabled. This API is the most cumbersome, but it can
be useful for loading full files with tail call optimization enabled (see
examples below). It is also the foundation of all of the other TCOMethod
APIs.
Be warned, there are a few gotchas. For example, even when using one of the APIs
provided by the tco_method
gem, require
, load
, and Kernel#eval
still
won't evaluate code with tail call optimization enabled without changing the
RubyVM
settings globally. More on the various limitations of the tco_method
gem are outlined in the docs in the
Gotchas
section.
Add this line to your application's Gemfile:
gem "tco_method"
And then execute:
$ bundle
Or install it yourself as:
$ gem install tco_method
Require the TCOMethod
library:
require "tco_method"
TCOMethod.with_tco
The fastest road to tail call optimized glory is the
TCOMethod.with_tco
method. Using
TCOMethod.with_tco
you can evaluate a block of code with tail call optimization enabled liked so:
TCOMethod.with_tco do
class MyClass
def factorial(n, acc = 1)
n <= 1 ? acc : factorial(n - 1, n * acc)
end
end
end
puts MyClass.new.factorial(10_000).to_s.length
# => 35660
It's worth noting that in the example above the actual optimized tail call
occurs outside of the TCOMethod.with_tco
block. TCOMethod.with_tco
is used
to compile code in such a way that tail call optimization is enabled. Once
compiled, the tail call optimized code can be invoked from anywhere in the
program.
TCOMethod::Mixin
Alternatively, you can extend a Class or Module with the
TCOMethod::Mixin
and let the TCO fun begin using helpers that act like method annotations.
To redefine an instance method with tail call optimization enabled, use
tco_method
:
class MyClass
extend TCOMethod::Mixin
def factorial(n, acc = 1)
n <= 1 ? acc : factorial(n - 1, n * acc)
end
tco_method :factorial
end
puts MyClass.new.factorial(10_000).to_s.length
# => 35660
Or alternatively, use tco_module_method
or tco_class_method
for a Module or Class method:
module MyFibonacci
extend TCOMethod::Mixin
def self.fibonacci(index, back_one = 1, back_two = 0)
index < 1 ? back_two : fibonacci(index - 1, back_one + back_two, back_one)
end
tco_module_method :fibonacci
end
puts MyFibonacci.fibonacci(10_000).to_s.length
# => 2090
TCOMethod.tco_eval
Finally, depending on your needs (and your love for stringified code blocks),
you can also use
TCOMethod.tco_eval
directly.
TCOMethod.tco_eval
can be useful in situations where the method_source
gem is unable to determine
the source of a particular block or for loading entire files with tail call
optimization enabled.
TCOMethod.tco_eval(<<-CODE)
class MyClass
def factorial(n, acc = 1)
n <= 1 ? acc : factorial(n - 1, n * acc)
end
end
CODE
MyClass.new.factorial(10_000).to_s.length
# => 35660
You can kind of get around the need for stringified code blocks by reading the
code you want to compile with tail call optimization dynamically at runtime, but
this approach also has downsides in that it goes around the standard Ruby
require
/load
process. For example, consider the Fibonacci
example broken across
two scripts, one script serving as a loader and the other script acting as a
more standard library:
# loader.rb
require "tco_method"
fibonacci_lib = File.read(File.expand_path("../fibonacci.rb", __FILE__))
TCOMethod.tco_eval(fibonacci_lib)
puts MyFibonacci.fibonacci(10_000).to_s.length
# => 2090
# fibonacci.rb
module MyFibonacci
def self.fibonacci(index, back_one = 1, back_two = 0)
index < 1 ? back_two : fibonacci(index - 1, back_one + back_two, back_one)
end
end
If you really want to get crazy, you can include the TCOMethod::Mixin
module
in the Module
class to add these behaviors to all Modules and Classes. To quote
VIM plugin author extraordinaire, Tim Pope, "I don't like to get crazy." Consider
yourself warned.
# Don't say I didn't warn you...
Module.include(TCOMethod::Mixin)
module MyFibonacci
def self.fibonacci(index, back_one = 1, back_two = 0)
index < 1 ? back_two : fibonacci(index - 1, back_one + back_two, back_one)
end
tco_module_method :fibonacci
end
puts MyFibonacci.fibonacci(10_000).to_s.length
# => 2090
Quirks with the method_source
gem:
TCOMethod.with_tco
use themethod_source
gem to retrieveTCOMethod.with_tco
can act strangely when used in more dynamic contexts likeirb
or pry
. Additionally, if the code to be evaluated is formatted inmethod_source
and/ortco_method
to determine the unambiguous source of the method or code block.Quirks with TCOMethod.with_tco
:
method_source
gem, the given block will be evaluated with a bindingrequire
, load
, and eval
will still load code without tail callTCOMethod.with_tco
. Each of these methods compiles code using the primaryRubyVM::InstructionSequence
object which honors the configuration specifiedRubyVM::InstructionSequence.compile_option
.Quirks with Module and Class annotations:
def
keyword.There are almost certainly more gotchas, so check back for more in the future if you run into weirdness while using this gem. Issues are welcome.
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)