alef-1::ruby::method_missing

method_missing, the price to pay.

Jean Lazarou
August 6, 2007

Preface

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.

Using the method_missing

What is our work context?

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.

Implement the n! notation

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.

The code so far

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.

What is the price to pay?

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.

Code enhancement

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.

Warning

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.

Optimization

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.

Benchmarking: Time to pay now!

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.

Merge all the approaches

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

Write the benchmark code

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 results

The benchmark was run with Ruby 1.8.5 on a Windows-XP system and a Linux/Ubuntu system.

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.

Download

Here is the whole code in one file. Run the code by executing: ruby method_missing_benchmark.rb.