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.
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]
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]
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]
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]
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.
The recursive rule by which the elements in the ancestors' chain list are ordered may be formulated like so:
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