Continuing adventures in adding methods to Ruby instances

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.

Comments