MetaRuby - Monkeying With Methods


In this series we learned how to dynamically create methods and classes, now we’ll look at how to monkey around with existing methods. We’ll take a look at how you can introspect on methods, call them, and then wrap up with a little metaprogramming to decorate them.

Ruby provides two ways to get methods, method and instance_method. Let’s see it in action:

class Person
  def initialize(name)
    @name = name
  end
  
  def greet(other)
    puts "Hi #{other}, I'm #{@name}"
  end
end

wallace = Person.new("Wallace")
method  = wallace.method(:greet)         #=> #<Method: Person#greet>
unbound = Person.instance_method(:greet) #=> #<UnboundMethod: Person#greet>

Notice that in one case we got a Method, and in the other an UnboundMethod. We’ll dig into the differences later.

Getting Information

Once you have a Method, you can inspect it to learn more about it:

method.source_location #=> ["/Users/adam/Projects/person.rb", 6]
method.original_name   #=> :greet
method.owner           #=> Person
method.arity           #=> 1

These are all handy methods, but source_location is a great tool if you ever want to either read the source of a method. It can also help if you suspect a method has been monkey patched:

Range.instance_method(:to_s).source_location 
#=> nil (Native methods return nil)

require "active_support/all"
Range.instance_method(:to_s).source_location #=> nil
#=> ["/Users/adam/.../active_support/core_ext/range/conversions.rb", 9] 

Although you will probably not use this in your daily Ruby code, it can be handy when debugging in irb.

Calling Methods

Method instances have similar semantics to Procs.

method.call("Mortimer")
# Hi Mortimer, I'm Wallace

["Grendle", "Horace", "Wilson"].each(&method)
# Hi Grendle, I'm Wallace
# Hi Horace, I'm Wallace
# Hi Wilson, I'm Wallace

method.call()
# ArgumentError: wrong number of arguments (0 for 1)

If you call a method, it’s just like invoking it directly. You can even convert it to a Proc using either the & operator, or by calling to_proc. This allows you to use a Method any place a block is expected. Furthermore, the method can be passed around, and it still retains a reference to its original object.

We’ve just been looking at bound methods. When we called Person.instance_method(:greet), we got back an UnboundMethod because there was no Person instance associated with it. You can’t call an UnboundMethod method directly, first you need to bind it to an instance:

unbound = Person.instance_method(:greet) #=> #<UnboundMethod: Person#greet>
person  = Person.new("Septimus")         #=> #<Person @name="Septimus">
bound   = unbound.bind(person)           #=> #<Method: Person#greet> 
bound.call("George")
# Hi George, I'm Septimus

You can get an unbound method from a bound method by calling unbind.

Decorating Methods

An UnboundMethod may not seem very interesting, but it’s a building block for some interesting metaprogramming. We’re going to extend Ruby’s Class, and write a method that lets us wrap other methods.

class Class
  def after_method(name, &block)
    original_method = instance_method(name)
    define_method(name) do |*args|
      original_method.bind(self).call(*args)
      block.call(*args)
    end
  end
end

Person.after_method(:greet) do |other|
  puts "Goodbye #{other}"
end

person.greet "Marvin"
# Hi Marvin, I'm Septimus
# Goodbye Marvin

Let’s walk through what’s going on here. First we extend Class directly by re-opening it, and then define a new method called after_method. This means that our method will be available on every Class. Next, we get a reference to the method we are wrapping using instance_method.

We define a new method with the same name as the old method, effectively replacing it. The new method takes a variable number of arguments using *args. The new method binds original_method to self, which is the object our wrapped method is being called upon. This gives us a Method which can then be called with the args array expanded back out. Finally, we call the block that was passed in. This all works because original_method retains the original method definition.

As of Ruby 2.1, defining a method returns a symbol representing the name of the method. It may seem minor, but using this with the technique we saw above, you can decorate methods as you create them. For instance, let’s declare that a method should be logged when it is called:

class Class
  def logged(name)
    original_method = instance_method(name)
    define_method(name) do |*args|
      puts "Calling #{name} with #{args.inspect}."
      original_method.bind(self).call(*args)
      puts "Completed #{name}."
    end
  end
end

class Meal
  def initialize
    @food = []
  end
  
  logged def add(item)
    @food << item
  end
end

meal = Meal.new
meal.add "Coffee"
# Calling add with ["Coffee"].
# Completed add.

Did you catch that? def add(item) returns :add, which becomes the name argument for our logged method.

Recap

Ruby’s Method and UnboundMethod classes give us the power to monkey around with methods.

  • Methods can be obtained with method and instance_method
  • Find out where a method is defined with source_location
  • bind UnboundMethods to call them
  • Methods can be wrapped by retaining the original method
  • Method definition returns a symbol

Have you come across any other neat things you can do with Method? Let me know!

More articles in this series