In the last post we looked at the hoops we had to go through to redefine a method on a single instance using a closure. In this post we’ll look at ways to make this easier on ourselves.
I should stress that I’m a Ruby n00bie, so take this all with a grain of salt. Please let me know if I’ve got it wrong.
First attempt
We found out last time that to add an instance method to a single object using a closure we had to obtain a reference to that instance’s metaclass (or eigenclass as it is also known) using the class << instance syntax. We could then use the metaclass’s private define_method via the send method.
class Greeter
def say_hello
puts "Hello World!"
end
end
greeters_metaclass = class << greeter; self; end
greeters_metaclass.send(:define_method, :say_hello) do
puts "G'day #{name}"
end
greeter.say_hello
#=> G'day Anonymous Dave
We can make this a bit easier by extracting this into a method. If we put it in a Ruby module, we can open up any class or instance and import this code as required.
module Meta
def define(name, &block)
meta = class << self; self; end
meta.send(:define_method, name, block)
end
end
And this works the same way as the previous code, just a bit neater:
# Open up Greeter class and import the Meta module:
class Greeter
include Meta
end
# Our greeter gains the new syntax:
name = "Anonymous Dave"
greeter.define(:say_hello) do
puts "G'day #{name}"
end
greeter.say_hello
#=> G'day Anonymous Dave
We can do better.
Second attempt
It would be really nice to be able to just write greeter.define.say_hello { puts "blah" }. Let’s make it work. We’ll need to make a define method that returns some sort of object that responds to any method, whether it exists or not. Ruby has a handy method_missing method we can define which will be invoked in the event it cannot resolve a particular method. We can just move the method creation into there:
module Meta
def define
meta = class << self; self; end
Creator.new(meta)
end
class Creator
def initialize(meta)
@meta = meta
end
def method_missing(symbol, *args, &block)
@meta.send(:define_method, symbol, block)
end
end
end
This then gives us a nicer syntax whenever we import the module:
name = "Anonymous Dave"
greeter.define.say_hello do
puts "G'day #{name}"
end
greeter.say_hello
#=> G'day Anonymous Dave
greeter.define.say_hello do
puts "Howdy!"
end
greeter.say_hello
#=> Howdy!
Parting thoughts
Like I said, I’m new to this stuff and have absolutely no idea if this is completely terrible. I do know that I’m really impressed with how easily even a newbie like me could add the syntax I wanted to absolutely everywhere in Ruby I could possibly need it, in a fairly neat and reusable way.