Meta-programming is often kind of mean. It has a tendency to be hard to understand and it can be quite destructive - causing methods to be modified or overwritten in non-obvious ways. I decided to write a post about a more courteous form of meta-programming. I picked up the technique from a blog post about method_missing and respond_to?. I realized that although I am comfortable with meta-programming, other developers might not feel the same way. So, here's a simple pattern that can make your meta-programming more readable and less destructive.
I want to create a module named Greetings so that when I extend my classes
with it I can declaratively add greetings to them.
class FrenchAndEnglish
extend Greetings
greeting "Bonjour!"
greeting "Hello!"
end
irb(main):001:0> FrenchAndEnglish.new.greet
Bonjour!
Hello!
=> nil
There are a couple of ways to accomplish this with Ruby. But before we jump into code, let's add these constraints to the system. After all, we are trying to write courteous code.
greet method should support the use of super to
delegate to parent definitions.greet method.For instance, consider the following use of the Greetings module. It supports
the use of super to delegate to previous implementations of greet and it
allows the base class to define it's own greet method.
class TriLingualWithEnglish
extend Greetings
greeting 'Hello!'
greeting 'Howdy!'
def initialize(message01, message02)
@message01 = message01
@message02 = message02
end
def greet
super rescue nil
puts "#{@message01} and #{@message02}!"
end
end
irb(main):001:0> TriLingualWithEnglish.new('Bonjour', 'Kalimera').greet
Hello!
Howdy!
Bonjour and Kalimera!
=> nil
Let's look at the most common meta-programming approaches to solving this problem.
instance_eval or class_eval might be acceptable, but
these options are generally destructive. They overwrite methods on the base
class and this breaks our non-destructive requirement. If we were able to solve
this problem by using alias method chains then we would definitely be entering
the territory of un-readable code. Also method chains would not allow us to use
super to call previous definitions of greet.super, but it also greatly restricts our flexibility. We
can no longer define an alternate greet method on the base class.Since these two approaches don't fit our courteous meta-programming requirements, here's the cool alternative that does.
module Greetings
def greeting(phrase)
m = Module.new do
define_method(:greet) do
super rescue nil
puts(phrase)
end
end
include m
end
end
By using the anonymous module m, and including it we are actually creating a
storage object in the class hierarcy for the method, instead of writing it on
the original class. This approach is non-destructive and allows us to maintain
a clean method chain that supports the use of super. The super rescue nil
line in our TriLingualWithEnglish class now calls into the anonymous module
and allows both greeting implementations to co-exist. The code is also very
readable since the programmer does not need to understand class_eval and
instance_eval. I would advocate this approach whenever possible since it
preserves the class hierarchy and avoids some of the less readable
meta-programming techniques.