I was reading Eloquent Ruby yesterday morning (buy a copy if you haven’t already), and it got me thinking about one of my “favorite” Ruby gotchas: defining #method_missing without a corresponding #respond_to?. E.g.:
class Liar def method_missing(*args) "Oops, I lied" end end l = Liar.new l.respond_to?(:foo) # => false l.foo # => "Oops, I lied"
The resulting code violates the Principle of Least Surprise, and often interacts in unexpected ways with other libraries.
I started wondering if it would be possible to define #method_missing and #respond_to? at the same time, at least for common #method_missing idioms. After some fiddling I came up with something that worked pretty well. Here’s how it looks:
class Foo extend MatchMethodMacros match_method(/Amake_me_a_/) do |name, *args| food = /Amake_me_a_(.*)$/.match(name.to_s)[1] "Make your own damn #{food}" end match_method(/Asudo_make_me_a/) do |name, *args, &block| food = /Asudo_make_me_a_(.*)$/.match(name.to_s)[1] "Coming right up, one #{food}" end def method_missing(name, *args) # match_method uses modules, so we can use super to delegate to # the generated #method_missing definitions. super rescue NoMethodError "We don't do that kind of thing here" end end foo = Foo.new foo.respond_to?(:fix_me_a_sandwich) # => false foo.respond_to?(:make_me_a_sandwich) # => true foo.respond_to?(:sudo_make_me_a_sandwich) # => true foo.fix_me_a_sandwich # => "We don't do that kind of thing here" foo.make_me_a_sandwich # => "Make your own damn sandwich" foo.sudo_make_me_a_sandwich # => "Coming right up, one sandwich"
And here’s the implementation:
module MatchMethodMacros def match_method(matcher, &method_body) mod = Module.new do define_method(:method_missing) do |method_name, *args| if matcher === method_name.to_s instance_exec(method_name, *args, &method_body) else super(method_name, *args) end end define_method(:respond_to_missing?) do |method_name, include_private| # Even though this is in the #respond_to_missing? hook we # still need to call 'super' in case there are other included # modules which also define #respond_to_missing? (matcher === method_name) || super(method_name, include_private) end end include mod end end
It turned out to be a relatively straightforward bit of metaprogramming. Breaking it down, it works like this:
match_methodis a “macro” – a method intended to be used at the class or module level to define other methods.match_methodtakes amatcher(anything which responds to ===, such as a Regexp) and a block. The matcher determines if the missing method name has been matched. The block becomes the body of the method.- An anonymous module is created to house the new methods. Putting the methods inside their own module makes it possible to make multiple calls to
match_methodwithout each one overwriting the last one’s work, as well as for the client class to also define its own explicitmethod_missing. - Inside the anonymous module, a new
#method_missingis defined. It uses thematcherto determine if the method being called is a match, and if so, it triggers themethod_bodyblock to be called in the context of the instance. Otherwise it passes to the next#method_missing(which will be Ruby’s default#method_missingif nothing else). - A method
#respond_to_missing?is also defined, which simply checks to see if thematchermatches the given method name inStringform. If not it passes to the next#respond_to_missing?usingsuper. Note that Ruby doesn’t allow use of the bare version ofsuper(which passes the original arguments along) inside a method defined withdefine_method. Instead I have to explicitly passa the arguments along.#respond_to_missing?is the Ruby 1.9 way of hooking intorespond_to?. Ordinarily it would free us from the need to invokesuperat all, because#respond_to?does that before checking#respond_to_missing?. But in this case we may have multiple definitions of#respond_to_missing?defined in differentmatch_method-generated modules all included in the same class, and thesuperis required to invoke all of them.If none of that made sense, I don’t blame you. This stuff sometimes hurts my head.
- Finally, the generated module with its
#method_missingand#respond_to_missing?methods is included into the invoking class.
The only obvious downside of this approach is that there’s no way I can find to pass a block into the instance_exec, so even though Ruby 1.9 allows passing blocks into blocks, it’s not possible to write #match_method methods which take blocks.
In the words of Joel Hodgson: what do you think, sirs?





