About michelada.io


By joining forces with your team, we can help with your Ruby on Rails and Javascript work. Or, if it works better for you, we can even become your team! From e-Commerce to Fintech, for years we have helped turning our clients’ great ideas into successful business.

Go to our website for more info.

Tags


Method transplanting in Ruby

20th November 2015

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.

View Comments