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:

1
2
  class Foo
  end

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

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

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

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

1
  Foo.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:

1
  Foo.ancestors # => [Foo, 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:

1
2
3
4
5
6
  module Bar
  end

  class Foo
    include Bar
  end

Then the ancestors' chain would change as follows:

1
  Foo.ancestors # => [Foo, Bar, Object, Kernel, BasicObject]

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

We can complicate the example:

1
2
3
4
5
6
7
8
9
10
  module Baz
  end

  module Bar
    include Baz
  end

  class Foo
    include Bar
  end

Ancestors chain:

1
  Foo.ancestors # => [Foo, Bar, Baz, 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:

1
2
3
4
5
6
  module Bar
  end

  class Foo
    prepend Bar
  end

Therefore:

1
    Foo.ancestors # => [Bar, Foo, Object, Kernel, BasicObject]

How the ancestors chain changes on class inheritance?

Let’s take a look at the examples below:

1
2
3
4
5
  class Fiz
  end

  class Foo < Fiz
  end

Ancestors chain:

1
  Foo.ancestors # => [Foo, Fiz, Object, Kernel, BasicObject]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
  module Bizz
  end

  class Fiz
    include Bizz
  end

  module Bar
  end

  class Foo < Fiz
    include Bar
  end

Ancestors chain:

1
  Foo.ancestors # => [Foo, Bar, Fiz, Bizz, 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:

1
2
3
4
  class Foo < BasicObject
  end

  Foo.ancestors # => [Foo, 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.

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