Defining #method_missing and #respond_to? at the same time

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

(Also available as a Gist)

It turned out to be a relatively straightforward bit of metaprogramming. Breaking it down, it works like this:

  1. match_method is a “macro” – a method intended to be used at the class or module level to define other methods.
  2. match_method takes a matcher (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.
  3. 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_method without each one overwriting the last one’s work, as well as for the client class to also define its own explicit method_missing.
  4. Inside the anonymous module, a new #method_missing is defined. It uses the matcher to determine if the method being called is a match, and if so, it triggers the method_body block to be called in the context of the instance. Otherwise it passes to the next #method_missing (which will be Ruby’s default #method_missing if nothing else).
  5. A method #respond_to_missing? is also defined, which simply checks to see if the matcher matches the given method name in String form. If not it passes to the next #respond_to_missing? using super. Note that Ruby doesn’t allow use of the bare version of super (which passes the original arguments along) inside a method defined with define_method. Instead I have to explicitly passa the arguments along.

    #respond_to_missing? is the Ruby 1.9 way of hooking into respond_to?. Ordinarily it would free us from the need to invoke super at 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 different match_method-generated modules all included in the same class, and the super is required to invoke all of them.

    If none of that made sense, I don’t blame you. This stuff sometimes hurts my head.

  6. Finally, the generated module with its #method_missing and #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?