Method transplanting in Ruby
I had the chance to watch Akira Matsuda’s live streaming presentation from RubyConf 2015; Ruby 2 Methodology. presentation name, the talk is about what you can do with methods in Ruby.
Recorded video is still not available, although his slides are on Speaker Deck https://speakerdeck.com/a_matsuda/ruby-2-methodology.
Matsuda’s presentation is a nice overview on how to work with methods in Ruby. It is not a talk about new features, it's more of a talk about his findings on how to use methods effectively in Ruby.
From all the cases presented, there was one feature that I found particularly interesting: Method Transplanting.
Method transplanting
Method transplanting is a way for you to unbound a method from a module and bind it back into a class. Here is the example Matsuda used in his slides:
module Greeter
def hello
p 'hello'
end
end
class Cat
define_method :hello, Greeter.instance_method(:hello)
end
Cat.new.hello
#=> "hello"
Method :hello
was successfully transplanted from module Greeter
into class Cat
.
You will probably say, “well, I have being doing this for a while including modules into classes, how is this different?”.
module Greeter
def hello
p 'hello'
end
end
class Cat
include Greeter
end
Cat.new.hello
#=> "hello"
You are right, kind of. By using include
and method transplanting you can achieve almost the same result, however, let's see how they are different.
For our example where the method was transplanted, if you ask the class to report its public_instance_methods
ignoring ancestors you will get:
Cat.public_instance_methods(false)
#=> [:hello]
Cat.new.respond_to?(:hello)
#=> true
Looks like :hello
truly belongs to Cat
. You unbounded :hello
method from the Greeter
module and then bounded it back to the Cat
class.
Now, by doing the same for the version where you include the module you get:
Cat.public_instance_methods(false)
#=> []
Cat.new.respond_to?(:hello)
#=> true
The :hello
method is not bound to the Cat
class, it's still bound to the Greeter
module.
The most interesting point of this feature is the possibility to cherry pick which methods you want to transplant from any module to your class.
Most of the time, when you consider including a module in a class, there is a concern due to the possibility of polluting its interface. If you are on Ruby 2.0 or better, you can now opt to simply transplant what you need.
Let's see another example.
module Greeter
def say_hi
puts "Hi!"
end
def say_bye
puts "Bye!"
end
end
class Cat
define_method :say_bye, Greeter.instance_method(:say_bye)
end
Cat.public_instance_methods(false)
#=> [:say_bye]
Cat.new.say_bye
#=> "Bye!"
Cat.new.respond_to?(:say_hi)
#=> false
In this example you are cherry picking only what you want to transplant to your class. By doing this you are keeping the class neat, avoiding any unnecessary method definitions from the Greeter
module.
Use module
Let's create a module which will define a class method named use
that will allow us to transplant methods with this signature use Greeter, only:[:say_bye]
.
module Use
def use(source, only: [])
if !only.respond_to?(:to_ary)
only = [only]
end
if only.empty?
only = source.public_instance_methods(false)
end
only.each do |method|
define_method method, source.instance_method(method)
end
end
end
This is an example on how you could transplant methods with the Use
module.
class Cat
extend Use
use Greeter, only: [:say_bye]
end
Cat.public_instance_methods(false)
#=> [:say_bye]
Cat.new.say_bye
#=> "Bye!"
Cat.new.respond_to?(:say_hi)
#=> false
The syntax is simpler, the param only
can accept an array with the symbol, or just a single symbol of methods that you want to transplant.
If the param only
is not present, then all the instance methods of our module are transplanted into our class. However, this is something that you want to avoid and simply use the old plain include
.
Ruby 2.2 or newer not only supports method transplanting from a module but also from a class.
Conclusions
Method transplanting is an elegant solution to keeping classes clean. This solution will work perfectly when a transplanted method has no dependencies on other methods from the original module considering unbound/bound do not track them.
I have been writing Ruby programs for several years now, but it still amazes me that there are so many features within the language that are not so popular or common.
Transplanting methods feel useful, especially in Rails development, where almost everything is breaking down to modules.
Also, you have to admit that saying “I’m transplanting these methods” instead of “I’m including these methods” sounds nicer.