Ruby’s dynamism is awe-inspiring. One of the methods that contribute to this is method_missing. method_missing
is defined in BasicObject
. method_missing
allows us to:
- handle errors
- delegate methods
- build DSLs
Let’s discuss the practical uses of this method.
What is Ruby’s method_missing
?
We define methods inside classes. Ruby traverses a method lookup path when an object calls a method, starting from the object’s class and up the object’s class’s ancestor chain to reach the method. If the method the object calls is available in the lookup path, Ruby calls it. On some occasions, the method isn’t present in the ancestor chain.
All method lookups that fail end up reaching a method called method_missing
. method_missing
lives inside the BasicObject class, the root of all classes.
For a class, Aircraft
, that mixes in no other modules or prepends other module or inherit other classes, this is how the traversal would look like (I’ve omitted the lookup through eigenclasses for simplicity):
method_missing
For Error Handling
Perhaps the most common use of method_missing
is its support for gracefully handling errors. When an object invokes a method, Ruby navigates the method lookup chain looking for the method to execute. On reaching BasicObject
, method_missing
is called. The interpreter raises an error at this point.
Here’s an example:
class Aircraft
attr_accessor :passengers
def initialize
@passengers = 0
end
def fly; end
def park; end
end
aircraft = Aircraft.new
aircraft.drift
Here’s what happens when you call an inexistent method on an object: When the search for a #drift
method in the inheritance tree ends in vain, then the method call aircraft.drift
will output:
<main>: undefined method 'drift' for
#<Aircraft:0x00007fe66c1591e0 @passengers=0> (NoMethodError)
It would be nice to be able to handle this error in a friendlier way. Ruby permits this by making programmers override method_missing
. method_missing
takes three parameters; a method name, its list of arguments and a block. The last two are optional.
To handle these errors, you intercept the call to BasicObject’s method_missing with your own. Here’s a rewrite of our Aircraft
code:
class Aircraft
attr_accessor :passengers
def initialize
@passengers = 0
end
def method_missing(method_name, \*args)
message = "You called #{method_name} with #{args}. This method doesn't exist."
raise NoMethodError, message
end
end
aircraft = Aircraft.new
aircraft.add_passengers(275)
With this change, we now have as output:
'method_missing': You called add_passengers with [275].
This method doesn't exist. (NoMethodError)
This looks more welcoming than the error we saw before. This example shows the extent to which Ruby allows programmers to own their code. A slightly more sophisticated example may involve the use of super
where if someone calls an inexistent method, you’d want first to perform some operation on the method and then only pass control to the original method_missing
method as shown in this example from Ruby’s documentation:
class Roman
def roman_to_int(str) # ...
end
def method_missing(symbol, *args)
str = symbol.id2name
begin
roman_to_int(str)
rescue
super(symbol, *args)
end
end
end
r = Roman.new
r.iv #=> 4
r.xxiii #=> 23
r.mm #=> 2000
r.foo #=> NoMethodError
With more complicated usages of method_missing
, come more significant responsibilities. When you override method_missing
, you have to make sure that whatever method you call inside it is available. Otherwise, you’ll end up going in circles and eventually get a SystemStackError
.
Expanding on the Aircraft
class, here’s something more meaningful. If a method call on an Aircraft
instance starts with “add”, we call public_send
on the instance passing it a setter method that sets the variable of whatever comes after “add”.
class Aircraft
attr_accessor :passengers
def initialize
@passengers = 0
end
def method_missing(method_name, *args, &block)
if method_name =~ /add_(.*)/
public_send("#{Regexp.last_match(1)}=", *args)
else
super
end
end
end
aircraft = Aircraft.new
aircraft.add_passengers(275)
p aircraft.passengers #=> 275
p aircraft.respond_to?(:add_passengers) #=> false
In the method_missing
method, if the method called on any instance of Aircraft
matches “add_”, we call a setter method and pass it the arguments that came with the method call.
However, it would help if you took note that respond_to
“add_passengers” returns false, this is not true, and that’s why it’s good practice always to override respond_to_missing?
method anytime you override method_missing
.
In our case, we add:
def respond*to_missing?(method_name, include_private = false)
method_name =~ /add*(.\*)/ || super
end
class Aircraft
attr_accessor :passengers
def initialize
@passengers = 0
end
def method_missing(method_name, *args, &block)
if method_name =~ /add_(.*)/
public_send("#{Regexp.last_match(1)}=", *args)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name =~ /add_(.*)/ || super
end
end
aircraft = Aircraft.new
aircraft.add_passengers(275)
p aircraft.passengers #=> 275
p aircraft.respond_to?(:add_passengers) #=> true
Method Delegation With method_missing
According to Wikipedia, delegation refers to evaluating a member (property or method) of one object (the receiver) in the context of another original object (the sender), this means, an object is asking another object to perform the actions in the method the receiver is calling.
You can use Ruby’s method_missing to delegate methods to another class.
Here’s a contrived example:
class Referee
def red_card?
puts "true"
end
end
class Linesman
def initialize(referee)
@referee = referee
end
def method_missing(method, *args)
@referee.send(method)
end
end
linesman = Linesman.new(Referee.new)
linesman.red_card? # => true
In the code above we call red_card?
on linesman
, an instance of Linesman
, but it’s an instance of Referee
that does the job, this is an example of some form of delegation.
Ruby provides a library called delegate in its Standard Library that you might want to check out first if you need to do serious method delegation,
Building DSLs And Libraries With method_missing
A domain-specific language (DSL) is a language with which you can do specific tasks. Unlike Ruby, a DSL like RSpec only useful for testing.
DSLs are relatively easier to write. DSLs are everywhere. A few examples are:
- Capistrano
- Rake
- Sinatra
- Rails Routing
- factory_bot
Ruby’s method_missing makes building DSLs accessible. Let’s try our shot with the simplest XML generator. Here’s a modified snippet I grabbed from The Ruby Programming Language book.
class XML
class XML
def initialize(output)
@output = output
end
def method_missing(tag, attributes = {})
@output << "<#{tag}"
if block_given?
@output << '>'
content = yield
@output << content.to_s if content
@output << "</#{tag}>"
else
@output << '/>'
end
nil
end
def self.generate(output, &block)
XML.new(output).instance_eval(&block)
end
end
This code allows us to write something like:
XML.generate(STDOUT) do
html do
head do
title "Page Title"
end
body do
strong do
"Strong Text"
end
end
end
end
Running this produces:
<html><head><title/></head><body><strong>Strong Text</strong></body></html>
And just like that, we’ve harnessed the power of method_missing
to create a DSL that generates XML.
An excellent example of a DSL that relies on method_missing
is The Late Jim Weirich’s Builder gem. The Builder gem generates XML markup from blocks, much like what we just wrote but more sophisticated.
Another example that might seem familiar is Ruby on Rails’s dependence on method_missing
to create dynamic finders like the find_by_*
group of methods in ActiveRecord.
Question On The Traversal For method_missing
I learned yesterday from a group that Ruby does the traversal for method_missing
twice. The first one is to reach the method_missing
in BasicObject
, and the second traversal is to check if you overrode method_missing
: this makes sense, otherwise, how would Ruby pick up the method_missing
you’ve overwritten.
I haven’t found any reference on this. Please share if you have some documentation somewhere about this fact.
Summary
We’ve seen how to use method_missing
to handle errors and to do method delegation. With the help of another metaprogramming construct, we saw how to use instance_eval
to write a DSL that generates XML. We looked at how Ruby traverses the ancestor chain to find methods and calls method_missing
if it finds nothing. We learned that it’s a good idea to overwrite respond_to_missing?
any time we overwrite method_missing?
; this covers the list of things you can do with method_missing
. If you can think of any other use cases, please share them in the comments below.