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.
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.