Responding to Calls to Undefined Methods
Problem
Rather than having Ruby raise a NoMethodError when someone calls an undefined method on an instance of your class, you want to intercept the method call and do something else with it.
Or you are faced with having to explicitly define a large (possibly infinite) number of methods for a class. You would rather define a single method that can respond to an infinite number of method names.
Solution
Define a method_missing method for your class. Whenever anyone calls a method that would otherwise result in a NoMethodError, the method_missing method is called instead. It is passed the symbol of the nonexistent method, and any arguments that were passed in.
Here's a class that modifies the default error handling for a missing method:
class MyClass def defined_method 'This method is defined.' end def method_missing(m, *args) "Sorry, I don't know about any #{m} method." end end o = MyClass.new o.defined_method # => "This method is defined." o.undefined_method # => "Sorry, I don't know about any undefined_method method."
In the second example, I'll define an infinitude of new methods on Fixnum by giving it a method_missing implementation. Once I'm done, Fixnum will answer to any method that looks like "plus_#" and takes no arguments.
class Fixnum def method_missing(m, *args) if args.size > 0 raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)") end match = /^plus_([0-9]+)$/.match(m.to_s) if match self + match.captures[0].to_i else raise NoMethodError. new(" undefined method '#{m}' for #{inspect}:#{self.class}") end end end 4.plus_5 # => 9 10.plus_0 # => 10 -1.plus_2 # => 1 100.plus_10000 # => 10100 20.send(:plus_25) # => 45 100.minus_3 # NoMethodError: undefined method 'minus_3' for 100:Fixnum 100.plus_5(105) # ArgumentError: wrong number of arguments (1 for 0)
Discussion
The method_missing technique is frequently found in delegation scenarios, when one object needs to implement all of the methods of another object. Rather than defining each method, a class implements method_missing as a catch-all, and uses send to delegate the "missing" method calls to other objects. The built-in delegate library makes this easy (see Recipe 8.8), but for the sake of illustration, here's a class that delegates almost all its methods to a string. Note that this class doesn't itself subclass String.
class BackwardsString def initialize(s) @s = s end def method_missing(m, *args, &block) result = @s.send(m, *args, &block) result.respond_to?(:to_str) ? BackwardsString.new(result) : result end def to_s @s.reverse end def inspect to_s end end
The interesting thing here is the call to Object#send. This method takes the name of another method, and calls that method with the given arguments. We can delegate any missing method call to the underlying string without even looking at the method name.
s = BackwardsString.new("I'm backwards.") # => .sdrawkcab m'I s.size # => 14 s.upcase # => .SDRAWKCAB M'I s.reverse # => I'm backwards. s.no_such_method # NoMethodError: undefined method 'no_such_method' for "I'm backwards.":String
The method_missing technique is also useful for adding syntactic sugar to a class. If one method of your class is frequently called with a string argument, you can make object.string a shortcut for object.method("string"). Consider the Library class below, and its simple query interface:
class Library < Array def add_book(author, title) self << [author, title] end def search_by_author(key) reject { |b| !match(b, 0, key) } end def search_by_author_or_title(key) reject { |b| !match(b, 0, key) && !match(b, 1, key) } end :private def match(b, index, key) b[index].index(key) != nil end end l = Library.new l.add_book("James Joyce", "Ulysses") l.add_book("James Joyce", "Finnegans Wake") l.add_book("John le Carre", "The Little Drummer Boy") l.add_book("John Rawls", "A Theory of Justice") l.search_by_author("John") # => [["John le Carre", "The Little Drummer Boy"], # ["John Rawls", "A Theory of Justice"]] l.search_by_author_or_title("oy") # => [["James Joyce", "Ulysses"], ["James Joyce", "Finnegans Wake"], # ["John le Carre", "The Little Drummer Boy"]]
We can make certain queries a little easier to write by adding some syntactic sugar. It's as simple as defining a wrapper method; its power comes from the fact that Ruby directs all unrecognized method calls to this wrapper method.
class Library def method_missing(m, *args) search_by_author_or_title(m.to_s) end end l.oy # => [["James Joyce", "Ulysses"], ["James Joyce", "Finnegans Wake"], # ["John le Carre", "The Little Drummer Boy"]] l.Fin # => [["James Joyce", "Finnegans Wake"]] l.Jo # => [["James Joyce", "Ulysses"], ["James Joyce", "Finnegans Wake"], # ["John le Carre", "The Little Drummer Boy"], # ["John Rawls", "A Theory of Justice"]]
You can also define a method_missing method on a class. This is useful for adding syntactic sugar to factory classes. Here's a simple factory class that makes it easy to create strings (as though this weren't already easy):
class StringFactory def StringFactory.method_missing(m, *args) return String.new(m.to_s, *args) end end StringFactory.a_string # => "a_string" StringFactory.another_string # => "another_string"
As before, an attempt to call an explicitly defined method will not trigger method_missing:
StringFactory.superclass # => Object
The method_missing method intercepts all calls to undefined methods, including the mistyped names of calls to "real" methods. This is a common source of bugs. If you run into trouble using your class, the first thing you should do is add debug statements to method_missing, or comment it out altogether.
If you're using method_missing to implicitly define methods, you should also be aware that Object.respond_to? returns false when called with the names of those methods. After all, they're not defined!
25.respond_to? :plus_20 # => false
You can override respond_to? to fool outside objects into thinking you've got explicit definitions for methods you've actually defined implicitly in method_missing. Be very careful, though; this is another common source of bugs.
class Fixnum def respond_to?(m) super or (m.to_s =~ /^plus_([0-9]+)$/) != nil end end 25.respond_to? :plus_20 # => true 25.respond_to? :succ # => true 25.respond_to? :minus_20 # => false
See Also
- Recipe 2.13, "Simulating a Subclass of Fixnum"
- Recipe 8.8, "Delegating Method Calls to Another Object," for an alternate implementation of delegation that's usually easier to use