Revisting replacing a Ruby instance method with a closure

Last month I looked at how to replace a Ruby method of a single object instance with a closure, before defining a module that could make this easier. Since then I’ve learnt another option which I thought I’d share as it helped me get a greater appreciation for Ruby modules.

Note: I am a Ruby n00bie, so take all this with a suitable amount of salt. If this (or the previous posts) violate Ruby conventions, or there is an idiomatic way of solving this, please let me know.

Quick recap

The problem is fully described in the original post, but it basically starts with this class:

class Greeter
    def say_hello
        puts "Hello World!"
    end
end
greeter = Greeter.new
greeter.say_hello
#=> Hello World!

I then wanted to replace say_hello on that single greeter instance with a method that would close over a local variable, like this:

name = "Anonymous Dave"
# replace say_hello on greeter so it puts "G'day #{name}"

greeter.say_hello
#=> G'day Anonymous Dave

name = "Clarence"
greeter.say_hello
#=> G'day Clarence

Standard reopening of the instance (or even the Greeter class) and redefining the method won’t work here, all because we have the pesky requirement of closing over our local name variable, which means we need to use a block (basically a lambda function for C# people). We can use Class.send to call the private define_method which takes a block, but that will add it to every instance of Greeter, not a single instance.

Modules to the rescue

We solved this in the original post by referencing the instance’s metaclass (aka eigenclass), but there is another way:

name = "Anonymous Dave"
new_say_hello = Module.new do
    self.send(:define_method, :say_hello) do
       puts "G'day #{name}"
    end
end

Here we’ve created a new anonymous module that sends define_method to create a say_hello method using a block, in the same way as we could have reopened the Greeter class and added it to every instance. The difference here is that this module has not been mixed in anywhere yet; we can choose exactly where we want to apply it. In this case, to our single instance:

# Mixin module to greeter instance to add our new say_hello method
greeter.extend new_say_hello

greeter.say_hello
#=> G'day Anonymous Dave

name = "Clarence"
greeter.say_hello
#=> G'day Clarence

# Other instances are unaffected by this:
another_greeter = Greeter.new
another_greeter.say_hello
#=> Hello World!

I think I still prefer the Meta module approach, but this way has the advantage of sticking closely to standard Ruby constructs and manages to avoid metaclasses.

What was most helpful to me out of this as a Ruby n00bie is the understanding that we can work using class scope within a module (avoiding metaclass shenanigans), then apply that scope selectively by including the module in a class, or by extending an instance with the module.

Comments