DEV Community

Cover image for Up the Ruby Ancestor Chain with method_missing
Mehdi FARSI for AppSignal

Posted on • Edited on • Originally published at blog.appsignal.com

Up the Ruby Ancestor Chain with method_missing

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.

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:

[
  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:

  • 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 in Object 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.

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:

  • 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.

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.

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:

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').

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:

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:

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:

  • 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à!

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.

Top comments (0)