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.