Method Missing
Your method is in another class.
Oct 17, 2021
Let’s try a shorter-form blog post today. Suppose you are bored and want to learn more about a Ruby library. You do the sensible thing: read the source, starting from a class the library’s clients would consume. You expect to find a plethora of method definitions or at least some mixins or class inheritance that might point you in the right direction. Instead you get the following:
class Foo
@bar = ...
def method_missing(method, *args)
@bar.send(method, *args)
end
end
If you are like me, then the first question you may have is, “Author. Where. Are. My. METHODS?”
Then the old wise-sounding voice in your head would probably echo something vague in response like:
Seek not what lies before your eyes.
And you would thus be enlightened.
Method Missing
The secret to this mystery lies, of course, in Ruby’s
method_missing
method. Normally, when a
method call is made to an object that does not define that method, NoMethodError
is thrown.
However, by implementing method_missing
, one is able to essentially catch calls before they fall
by specifying behavior that gets executed dynamically at runtime. For instance, in our toy example,
method_missing
catches method calls in Foo
, gets the method’s id, and uses
send
to redirect the method call to @bar
instead.
A major use of method_missing
seems to be in providing concise implementations of
delegation (our
snippet above is actually an example of this). Delegation usually is more verbose. For instance,
in Java, one would have to manually redefine every method being delegated in the parent’s class to
achieve the same functionality. Ruby’s method_missing
seems to make the whole affair shorter to
write and easier to maintain.
Another interesting application of method_missing
is in creating
domain-specific languages (DSLs)
by parsing method names. As an example, Kim Bekkelund has a very concise
XML DSL implementation using method_missing
.
method_missing
is also more generally a case of
metaprogramming, a feature of some languages that
allows code to be treated as data. While metaprogramming is not limited to Ruby, Ruby seems to be
particularly (in)famous for it.
A Hypothetical Pitfall
Like all things method_missing
can be overused and abused. For one, method_missing
has very high
reach since it can respond to any undefined method. Sometimes, this can lead to unexpected behavior.
Let’s contrive an example. Suppose we are making a dictionary API. We love method_missing
for some reason and decide to use it to write our dictionary:
class DictionaryAPI
def method_missing(method, *args)
method_name = method.to_s
if method_name.match(/^define_/)
term = method_name.sub('define_', '')
return {
:term => term,
:definition => @external_source.retrieve_definition(term),
}
else
raise NoMethodError
end
end
end
Our dictionary parses terms from method calls of the form define_{TERM}
and then retrieves
the definitions from an external source before returning a response.
This works fine for most common words:
d = Dictionary.new
puts d.define_dog
# { :term => "dog", :definition => ... }
But what if someone decides they want to find the definition for the term “singleton_method”?:
puts d.define_singleton_method
# dictionary.rb:18:in `define_singleton_method': wrong number of arguments (given 0, expected 1..2) (ArgumentError)
# from dictionary.rb:18:in `<main>'
Oops, define_singleton_method
is a method that already exists for all objects. Back to the drawing board…
This example is admittedly contrived. In this case, Ruby has also stopped us
very quickly because the function arity differs, so little harm was done.
However, what if this API existed at a larger scale? Perhaps DictionaryAPI
inherits
from a long line of ancestors, and we had a method name that silently collided with a
name from one of the ancestors. Perhaps there is no collision now, but there will be one in the
future due to a change in an ancestor.
Furthermore, it would be hard to catch this collision in testing since most people would just test common words like “dog” and see expected functionality. And what happens once someone does eventually discover the collision? The whole API needs to change. What if we end up colliding again with something else after the changes?
The main issue is that although name collision itself is not specific to method_missing
, by using
method_missing
, we open up the potential to collide with an infinite number of function
names rather than just one we manually defined.
Because of this, I feel that method_missing
and metaprogramming overall are double-edged swords.
Metaprogramming opens a lot of doors. On one hand, it allows for a crazy amount of functionality
to be implemented with very little code. On the other hand, using metaprogramming haphazardly can
lead to a lot of unforeseen issues, such as the method name collision scenario. This means that
much more care and attention needs to be taken when using it, and these mental acrobatics may
not be worth the benefits of metaprogramming at the end of the day.