Basics of Ruby Method Dispatch System (Part 1)
In this post, I will explain the basics of how a method call works in Ruby. I’ll assume that readers have some familiarity with Ruby language.
The post is in two parts. The first part covers what you need to know about typical Ruby program. It covers: ancestors hierarchy, class inheritance, and module include
and prepend
.
The second part digs into tools that are used less often. It covers: singleton method, singleton class, and class methods.
Overview
The key concept to understand is an object’s ancestors. An object’s ancestors include all classes and modules that it inherits from.
When a method is called on an object, Ruby interpreter browses the object’s class to find the definition of the method. If the definition is not found, the interpreter moves up the ancestors hierarchy and searches for the definition of that method. If the interpreter fails to find the definition of the method in the highest ancestor, it starts the next procedure.
The interpreter traverses the ancestors again, looking for the definition of method_missing
method this time. If there’s no method_missing
defined in the ancestors, then it returns NoMethodError
.
To summarize in a list:
- Search the class of the object for the definition of the method.
- Continue moving up the ancestors hierarchy and repeat the search.
- Search the class of the object for the definition of
method_missing
. - Traverse the ancestors hierarchy to find the definition of
method_missing
. - Return
NoMethodError
.
method_missing
is an essential tool used in metaprogramming, allowing programmers to respond to undefined methods in runtime. But it’s outside the scope of this post so I won’t mention it again.
Level 0: Basic Class
We’ll first look at the simplest construction typically seen in Ruby to go over the foundation. This part will be the longest in this post, because we’ll go over basic concepts and tools we’ll use.
We defined BasicClass
with an instance method basic_class_method
, then created a new instance of BasicClass
called basic_class_instance
.
class
method is defined in Class
in Ruby standard library. When called on an object, it returns the class of the object. As you can see, basic_class_instance
is an instance of BasicClass
. Nothing surprising here.
But it’s more interesting to see that BasicClass
is an instance of Class
. Every class you define in Ruby is an instance of Class
class.
In Ruby, everything is an object, which can mean a lot of different things. But for this particular aspect, let’s just think about how an object is constructed. In most OOP languages, an object is constructed as an instance of a class. But everything, including class, is an object in Ruby. So a class that you defined is constructed as an instance of a class called Class
. If this concept is new to you, take some time to think about it. There’s a bit of recursive thinking here.
ancestors
method is defined in Module
class of Ruby standard library. It shows the ancestors of a class or a module.
BasicClass
has three ancestors: Object
, Kernel
, BasicObject
. All Ruby objects inherit from these three modules.
Class
, which is the class of BasicClass
, has four ancestors: Module
, Object
, Kernel
, BasicObject
. Interestingly, Class
is a subclass of Module
in Ruby. It might look quite strange, but that’s how Ruby implements it.
Class
has some special capabilities that Module
does not, for example constructing an object. A discussion about Class
and Module
warrants a separate post, so I won’t talk about it here. Let’s move to the next tool.
instance_method
is defined in Module
, which returns an UnboundMethod
that represents the instance method given in its arguments. In our specific case, it means that basic_class_method
is an UnboundMethod
that is defined in BasicClass
under the name of basic_class_method
. The last part might seem redundant, but it’s useful when dealing with aliased methods.
We will just use instance_method
to see where the instance method is defined in the ancestors hierarchy, so I will not discuss what UnboundMethod
means in this post.
Level 1: Superclass and Subclass
In this part we will look at another typical structure: superclass and subclass. We’ll just use the tools we introduced in the previous part to analyze the structure without introducing new concepts.
We created a new SuperClass
, and had BasicClass
inherit from it. Let’s look at their ancestors.
BasicClass
has another ancestor called SuperClass
. Nothing unexpected here.
Let’s look at instance_method
next.
There’s nothing notable in the first three cases. SuperClass
has super_class_method
, BasicClass
has basic_class_method
, and SuperClass
does not have basic_class_method
.
The last one provides some interesting information. It tells us that BasicClass
has super_class_method
as an instance method, but the method is defined in SuperClass
.
In terms of method dispatch system, it means that Ruby interpreter traversed up a level in the ancestors hierarchy to find the definition of the method.
Level 2: Including and Prepending Modules
In Ruby, module mixin is used alongside class inheritance to organize data and functions and provide namespace. When a module is mixed in, its constants, methods, and module variables are appended to the receiving module. Let’s see how module mixin is represented in ancestors hierarchy.
There are four new modules: ModuleIncludedToBasicClass
, ModulePrependedToBasicClass
, ModuleIncludedToSuperClass
, ModulePrependedToSuperClass
. Their names should reveal their responsibilities clearly. There is also an instance method defined_in
defined in every module and class. It prints where it is defined and then calls super
to call a method with the same name defined higher up in the ancestors hierarchy.
There are two ways to mixin module, which are both defined in Module
in standard Ruby library. include
is the traditional method, and prepend
was introduced in Ruby 2.0. Their differences are easier to understand by looking at the ancestors hierarchy.
As you can see, prepend
puts the prepended module before the receiving module, whereas include
puts the included module after the receiving module. This has implications in overriding methods and super
calls.
You can see that the order of method call follows the ancestor hierarchy. The NoMethodError
at the end occurs because there’s no defined_in
method in Object
.
A valuable insight here is that module mixin is just another way of implementing inheritance and not something magical. It is, however, a very easy to use way to implement multiple inheritance.
Calling instance_method
tells us where the instance methods available to BasicClass
are actually defined. There’s nothing unexpected here.
Interlude
This should cover the basics of Ruby method dispatch system. For most of Ruby programming, it would be sufficient to just understand the material so far.
In the next part, I will discuss metaclass and singleton methods. Those are used much less often, and require a deeper understanding of Ruby.