The Ruby language calls a method named method_missing
if,
while executing some code, it runs into a method that was not defined. Of course,
before calling method_missing
, it searches everywhere (in the
object, in its class and in the class hierarchy.
The default implementation, in the Object
class,
raises a NoMethodError
exception. To change the default behavior,
implement the method_missing
and create your own handling. The
method signature is method_missing(method, *args)
. The method
parameter is the symbol of the method that is missing and the args
parameter is an array with the calling arguments.
This article gives explanations about the use of method_missing
A common pattern is using the method_missing
to extend
the available methods without needing to declare all of them. It is useful for
methods that all are doing similar things. The missing method name actually
contains parts that are used as arguments.
In the first part we look at some example, showing this technique. In the second part we run some benchmark.
method_missing
Let's create a Computer
class that contains a factorial method
(you know the famous n! thing).
class Computer def factorial n raise ArgumentError if n < 0 f = 1 n.downto(1) do |i| f = f * i end f end end
The code is pretty basic, it implements in a straight way the factorial computation.
If we want to use the method, we must create a Computer
object and call
the factorial
method passing some number. We would like to add some cool
way to use it, instead of:
computer = Computer.new puts computer.factorial(4)
We would like to use some notation close to the usual notation:
computer = Computer.new puts computer._4!
Obviously, we cannot create methods for every integer, to do it we use the
method_missing
.
Here is the way we implement the method_missing
in our
Computer
class:
def method_missing(meth, *args) meth.to_s =~ /_([0-9]*)!/ return super if !$1 factorial($1.to_i) end
First, we check if the missing method name matches the notation we want: starts with
an underscore, followed by digits and ends with an exclamation mark. We use a regular expression
to actually perform the check (/_([0-9]*)!/
).
Next, if the method name does not follow our pattern (see the cryptic if !$1
test), we call the method_missing
of the super class (Object
here).
Otherwise, we convert the digits, from the method name, to an integer and call the
factorial
method.
Our Computer
class now looks like:
class Computer def factorial n raise ArgumentError if n < 0 f = 1 n.downto(1) do |i| f = f * i end f end def method_missing(meth, *args) meth.to_s =~ /_([0-9]*)!/ return super if ! $1 factorial($1.to_i) end end
If we use the special notation (_<digits>!
) the method_missing
implementation extracts the number, from the method name, and calls the factorial
method to get the result. Each time and for any method the same processing happens.
The method_missing
call is the last thing the Ruby interpreter tries to do
while executing code. We can expect that the use of the method_missing
would
be less efficient. But we cannot replace the implementation presented above by a real
implementation. Let's try some other strategies and then compare them.
Instead of using the method_missing
each time the same method name is
used, we can dynamically create a new method so that, next time it is called, the method
does exist. Here is the same implementation, as before, with the enhancement shown in
yellow:
def method_missing(meth, *args) meth.to_s =~ /_([0-9]*)!/ return super if ! $1 self.class.send(:define_method, meth) {factorial($1.to_i)} factorial($1.to_i) end
The highlighted code creates a new method by sending a :define_method
message
to the (Computer
) class, passing the method name (the method's symbol recieved
by method_missing
). We pass a block to the call, which implements the
factorial call with the right value.
We still need to compute and return the factorial, as before, for the first call.
If we don't know how the code is going to be used, using the example we present here may end with a big number of methods... mainly, using this approach is not recommended for a library that publishes the factorial method.
We can go one step further when using the improvement presented, we can optimize the code. As we are adding a new method, we can easily add a method that returns the result without even computing it anymore:
def method_missing(meth, *args) meth.to_s =~ /_([0-9]*)!/ return super if ! $1 f = factorial($1.to_i) self.class.send(:define_method, meth) {f} f end
We store the result in a variable, named f
, add a method that returns the result,
as the factorial value is not going to change next time (nothing changes really...), and
return the result, for the first-time call.
Let's try to see how efficient the implementation is. We are going to us the
Benchmark
support of Ruby and compare the different type of calls.
We are going to keep all the code in one file.
The method_missing
is going to behave in three different ways depending
on the value of a instance variable, named @mode
. The variable is intialized
on instance creation.
def method_missing(meth, *args) meth.to_s =~ /_([0-9]*)!/ return super if ! $1 f = factorial($1.to_i) if @mode == :optimized then self.class.send(:define_method, meth) {f} elsif @mode == :basic then self.class.send(:define_method, meth) {factorial($1.to_i)} end f end
The initialize
method sets the value of the @mode
variable, it
defaults to :missing_always
. The other valid values are: :basic
and :optimized
.
def initialize mode = :missing_always @mode = mode end
We add a require statement of the benchmark
module.
We write the benchmark, with a normal call as:
Benchmark.bm do |x| computer = Computer.new x.report("Normal method ") do 10000.downto(1) do computer.factorial(4) computer.factorial(20) computer.factorial(10) computer.factorial(5) end end end
The inner part is a loop that computes four factorials 10.000 times. It can be seen as
a benchmark section, the report is named Normal method. During the execution of
this code the method_missing
is not called and produces the reference
statistics.
After the first benchmark, we write the code for the one that always calls
the method_missing
, in the same benchmark block:
computer = Computer.new x.report("Always missing") do 10000.downto(1) do computer._4! computer._20! computer._10! computer._5! end end
Before going on, as the other tests follows the same scheme, let's refactor the code. We
extract a method, named compute_factorials
:
def compute_factorials computer 10000.downto(1) do computer._4! computer._20! computer._10! computer._5! end end
And replace the previous code with a call to the new method:
computer = Computer.new x.report("Always missing") do compute_factorials computer end
Witing the other tests are pretty simple, now:
computer = Computer.new :basic x.report("Create method ") do compute_factorials computer end
When the code above executes, it adds methods to the Computer
class and
we need to remove them before executing the fourth part:
class Computer remove_method :_4! remove_method :_5! remove_method :_10! remove_method :_20! end
The class is ready to execute in its initial state:
computer = Computer.new :optimized x.report("Optimized ") do compute_factorials computer end
Here is the whole benchmark code:
def compute_factorials computer 10000.downto(1) do computer._4! computer._20! computer._10! computer._5! end end Benchmark.bm do |x| computer = Computer.new x.report("Normal method ") do 10000.downto(1) do computer.factorial(4) computer.factorial(20) computer.factorial(10) computer.factorial(5) end end computer = Computer.new x.report("Always missing") do compute_factorials computer end computer = Computer.new :basic x.report("Create method ") do compute_factorials computer end class Computer remove_method :_4! remove_method :_5! remove_method :_10! remove_method :_20! end computer = Computer.new :optimized x.report("Optimized ") do compute_factorials computer end end
The benchmark was run with Ruby 1.8.5 on a Windows-XP system and a Linux/Ubuntu system.
Windows user system total real Normal method 0.681000 0.000000 0.681000 ( 0.681000) Always missing 1.161000 0.000000 1.161000 ( 1.161000) Create method 0.822000 0.000000 0.822000 ( 0.821000) Optimized 0.070000 0.000000 0.070000 ( 0.071000)
Linux user system total real Normal method 0.950000 0.110000 1.060000 ( 1.092198) Always missing 1.520000 0.140000 1.660000 ( 1.684640) Create method 1.210000 0.090000 1.300000 ( 1.330977) Optimized 0.100000 0.030000 0.130000 ( 0.154522)
Obviously, the optimized version is the best. Otherwise we see that the normal call remains the most efficient. Creating methods improves the performance.
To get an idea on what the gain is, we can say that (on Windows) adding a method, instead
of using the method_missing
, results in a 30% gain. Compared with the optimized,
the gain reaches 93% (very specific to this case). But even the approach that adds a method
has a 21%-overhead compared to normal calls.
Here is the whole code in one file. Run the code by executing:
ruby method_missing_benchmark.rb
.