The ancestors chain in Ruby explained

 ruby    introspection  

The ancestors' chain in Ruby is an essential bit of knowledge for the ones who aim to achieve a good understanding of the language. Let's dive into details.

Where all those methods come from?

Consider the following example:

  class Hotel
  end
  

We just created the empty Hotel class. No methods are defined yet. See what happens when we check what methods are available to invoke:

  Hotel.instance_methods # => [:remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :public_send, ...]
  Hotel.instance_methods.size # => 56
  

There are lots of methods that are predefined and are kept in classes and modules outside of our class Hotel.

In fact, the Hotel class has Object as its superclass:

  Hotel.superclass # => Object
  

The methods we just discovered belong to Object, and Kernel.

When we invoke a method, Ruby will look for it in the current class/module. Otherwise, if the given method is missing from the current class/module, Ruby will look for the method in the next class/module. This forms the ancestors chain - the way the interpreter copes with method invocations within the class hierarchy.

To view the ancestors chain of a given class we make a call like this one:

  Hotel.ancestors # => [Hotel, Object, Kernel, BasicObject]
  

How actually the ancestors chain works?

How the ancestors chain changes on module inclusion?

If for some reason we include a module in our class, let’s say:

  module Housekeeping
  end

  class Hotel
    include Housekeeping
  end
  

Then the ancestors' chain would change as follows:

  Hotel.ancestors # => [Hotel, Housekeeping, Object, Kernel, BasicObject]
  

Any method in Hotel whose name collides with a method name from the Housekeeping module will be invoked over the one in Housekeeping or elsewhere in the ancestors chain.

We can complicate the example:

  module TaskHelper
  end

  module Housekeeping
    include TaskHelper
  end

  class Hotel
    include Housekeeping
  end
  

Ancestors chain:

  Hotel.ancestors # => [Hotel, Housekeeping, TaskHelper, Object, Kernel, BasicObject]
  

How the ancestors chain changes on module prepending?

On the other side, prepend will make the ancestors chain look for the method first in the module and then in the current class/module:

  module Housekeeping
  end

  class Hotel
    prepend Housekeeping
  end
  

Therefore:

    Hotel.ancestors # => [Housekeeping, Hotel, Object, Kernel, BasicObject]
  

How the ancestors chain changes on class inheritance?

Let’s take a look at the examples below:

  class Building
  end

  class Hotel < Building
  end
  

Ancestors chain:

  Hotel.ancestors # => [Hotel, Building, Object, Kernel, BasicObject]
  

This one is self-explanatory, and now let’s see what happens when we mix modules and class inheritance:

  module ManagementHelper
  end

  class Building
    include ManagementHelper
  end

  module Housekeeping
  end

  class Hotel < Building
    include Housekeeping
  end
  

Ancestors chain:

  Hotel.ancestors # => [Hotel, Housekeeping, Building, ManagementHelper, Object, Kernel, BasicObject]
  

Namespace pollution

The class Object has Kernel mixed in and BasicObject as its superclass.

In some bizarre cases, we need to avoid namespace pollution from the Kernel module.

This is done when we inherit from BasicObject directly - the parent of all classes in Ruby.

Here is how the ancestors chain is different from usual having such parent class:

  class Hotel < BasicObject
  end

  Hotel.ancestors # => [Hotel, BasicObject]
  

The usage of this technique exceeds the scope of this blog post.

Summary

The recursive rule by which the elements in the ancestors' chain list are ordered may be formulated like so:

  1. Ruby looks for the method in the current class.
  2. Ruby looks in each of the included modules starting from the last included module and going up to the first one.
  3. All included modules in the currently inspected module are crawled in the same fashion as step 2 until the last module is checked.
  4. When all modules are crawled, the superclass of the given class is inspected.
  5. The included modules of the superclass are analyzed in the same way as step 2.
  6. Finally Ruby looks at Object, Kernel, BasicObject since Object is the implicit parent of the classes in Ruby.

prepend is excluded from the aforementioned description for simplicity but the only change it brings is that it forces Ruby to look first in the prepended module instead of the current class/module.

The last step when no suitable method was found is to invoke BasicObject#method_missing. More about it here: method_missing explained

References