Subscribe to
Ruby Magic
Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.
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!
Let’s start with the fundamental rules of ancestor chains in Ruby:
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | module Auth end module Session end module Iterable end class Collection prepend Iterable end class Users < Collection prepend Session include Auth end p Users.ancestors |
produces:
1 2 3 4 5 | [ Session, Users, Auth, # Users Iterable, Collection, # Collection Object, Kernel, BasicObject # Ruby Object Model ] |
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:
Users
Users
classUsers
class’ included modulesCollection
class’ prepended modules — as the direct parent of UsersCollection
classCollection
class’ included modules — noneObject
class — the default inheritance of any classKernel
module — included in Object
and holding core methodsBasicObject
class — the root class in RubySo, the order of appearance for a given traversed class or module is always as follows:
The ancestor chain is mainly traversed by Ruby when a method is invoked on an object or a class.
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.
1 2 3 4 5 6 7 8 | class Collection < Array end Collection.ancestors # => [Collection, Array, Enumerable, Object, Kernel, BasicObject] collection = Collection.new([:a, :b, :c]) collection.each_with_index # => :a |
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:
each_with_index message
=> NOeach_with_index message
=> NOeach_with_index message
=> YESSo, 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.
1 2 3 4 5 | class Collection end c = Collection.new c.search('item1') # => NoMethodError: undefined method `search` for #<Collection:0x123456890> |
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.
BasicObject#method_missing
MethodGuess 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Collection def initialize @collection = {} end def method_missing(method_id, *args) if method_id[-1] == '=' key = method_id[0..-2] @collection[key.to_sym] = args.first else @collection[method_id] end end end collection = Collection.new collection.obj1 = 'value1' collection.obj2 = 'value2' collection.obj1 # => 'value1' collection.obj2 # => 'value2' |
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'
).
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:
1 2 3 4 5 6 | HTML.p 'hello world' # => <p>hello world</p> HTML.div 'hello world' # => <div>hello world</div> HTML.h1 'hello world' # => <h1>hello world</h1> HTML.h2 'hello world' # => <h2>hello world</h2> HTML.span 'hello world' # => <span>hello world</span> HTML.p "hello #{HTML.b 'world'}" # => <p>hello <b>world</b></p> |
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:
1 2 3 4 5 | module HTML def HTML.method_missing(method_id, *args, &block) "<#{method_id}>#{args.first}</#{method_id}>" end end |
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:
<br/>
for exampleBut 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.
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à!
Guest author Mehdi Farsi is the founder of www.rubycademy.com which will offer cool courses to learn Ruby and Ruby on Rails when it launches soon.