Clutch your carry-on luggage, because today we'll travel all the way up the ancestor chain. We'll follow a method call and see how it goes up the chain as well as find out what happens if the method is missing. And because we love to play with fire, we won't stop there but continue on to playing with fire overriding the BasicObject#method_missing
. If you pay attention, we might also use it in a practical example. No guarantees though. Let's go!
The Ancestor Chain
Let's start with the fundamental rules of ancestor chains in Ruby:
- Ruby only supports single inheritance
- It also allows an object to include a set of modules
In Ruby, the ancestor chain is composed of the traversal of all the inherited classes and modules for a given class.
Let’s have a look at an example to show you how the ancestor chain is handled in Ruby.
produces:
First, we call the ancestors
class method to access the ancestor chain of a given class.
We can see that a call to Users.ancestors
returns an array of classes and modules that contain, in order:
- The prepended modules of
Users
- The
Users
class - The
Users
class’ included modules - The
Collection
class’ prepended modules — as the direct parent of Users - The
Collection
class - The
Collection
class’ included modules — none - The
Object
class — the default inheritance of any class - The
Kernel
module — included inObject
and holding core methods - The
BasicObject
class — the root class in Ruby
So, the order of appearance for a given traversed class or module is always as follows:
- The prepended modules
- The class or module
- Its included modules
The ancestor chain is mainly traversed by Ruby when a method is invoked on an object or a class.
The Method Lookup Path
When a message is sent, Ruby traverses the ancestor chain of the message receiver and checks if any of them responds to the given message.
If a given class or module of the ancestor chain responds to the message, then the method associated with this message is executed and the ancestor chain traversal is stopped.
Here, the collection.each_with_index
message is received by the Enumerable module. Then the Enumerable#each_with_index
method is called for this message.
Here, when collection.each_with_index
is called, Ruby checks if:
- Collection responds to the
each_with_index message
=> NO - Array responds to the
each_with_index message
=> NO - Enumerable responds to the
each_with_index message
=> YES
So, from here, Ruby stops the ancestor chain traversal and calls the method associated with this message. In our case, the Enumerable#each_with_index
method.
In Ruby, this mechanism is called the Method Lookup Path.
Now, what happens if none of the classes and modules that compose a given receiver’s ancestor chain respond to the message?
BasicObject#method_missing
Enough with playing nice! Let's break things, developer style: by throwing exceptions. We'll implement a Collection
class and call an unknown method on one of its instances.
Here, the Collection
class doesn't implement a search
method. So a NoMethodError
is raised. But where does this error raising comes from?
The error is raised in the BasicObject#method_missing
method. This method is called when the Method Lookup Path ends up not finding any method corresponding to a given message.
Okay... but this method only raises a NoMethodError
. So it would be great to be able to override the method in the context of our Collection
class.
Overriding the BasicObject#method_missing
Method
Guess What? It’s totally fine to override method_missing
as this method is also subject to the mechanism of Method Lookup Path. The only difference with a normal method is that we’re sure that this method will be found at least once by the Method Lookup Path.
Indeed, the BasicObject class
— which is the root class of any class in Ruby — defines a minimal version of this method. Classic Ruby Magic, n'est pas?
So let’s override this method in our Collection class
:
Here, the Collection#method_missing
acts as a delegator to the @collection
instance variable. Actually, this is the way Ruby roughly handles object delegation — c.f: the delegate
library.
If the missing method is a setter method (collection.obj1 = 'value1'
), then the method name (:obj1
) is used as the key and the argument ('value1'
) as the value of the @collection
hash entry (@collection[:obj1] = 'value1'
).
An HTML Tag Generator
Now that we know how the method_missing
method works behind the scenes, let’s implement a reproducible use case.
Here, the goal is to define the following DSL:
To do so, we’re going to implement the HTML.method_missing
method in order to avoid defining a method for each HTML tag.
First, we define an HTML
module. Then we define a method_missing
class method in this module:
Our method will simply build an HTML tag using the missing method_id
— :div
for a call to HTML.div
, for example.
Note that class methods are also subject to the Method Lookup Path.
We could enhance our HTML tag generator by:
- using the block argument to handle nested tags
- handling single tags —
<br/>
for example
But note that with a few lines of code, we are able to generate a huge amount of HTML tags.
So, to recap:
method_missing
is a good entry point to create DSLs where most of the commands will share a set of identified patterns.
Conclusion
We went all the way up the ancestor chain in Ruby and dove into BasicObject#method_missing
. BasicObject#method_missing
is part of the Ruby Hook Methods. It is used to interact with objects at some precise moments in their lifecycle. Like any of the other Ruby Hook Methods, this hook method has to be used carefully. And by carefully, we mean that it should never modify the behaviors of the Ruby Object Model—except when you are playing around with it or writing a blog post on it ;-)
Voilà!