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
andinstance_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!