Understanding Ruby Method Lookup
Honeybadger Staff
Posted on July 8, 2021
This article was originally written by Kingsley Silas on the Honeybadger Developer Blog.
What do you think happens when you call a method? How does Ruby decide which method to call when there’s another method with the same name? Have you ever wondered where the method is housed or sourced from?
Ruby employs a defined "way" or "pattern" to determine the right method to call and the right time to return a “no method error”, and we can call this "way" the Ruby Method Lookup Path.
In this tutorial, we’ll be diving into Ruby’s method lookup. At the end, you’ll have a good understanding of how Ruby goes through the hierarchy of an object to determine which method you’re referring to.
To fully grasp what we'll be learning, you'll need to have a basic understanding of Ruby. While we'll mention things like modules and classes, this will not be a deep dive into what they do. We'll only cover the depth needed to reach the goal of this tutorial: show you how Ruby determines the message (method) you're passing to an object.
Overview
When you call a method, such as first_person.valid?
, Ruby has to determine a few things:
- Where the method
.valid?
is defined. - Are there multiple places where the
.valid?
method is defined? If so, which is the right one to use in this context.
The process (or path) Ruby follows in figuring this out is what we call method lookup. Ruby has to find where the method was created so that it can call it. It has to search in the following places to ensure it calls the right method:
- Singleton methods: Ruby provides a way for an object to define its own methods; these methods are only available to that object and cannot be accessed by an instance of the object.
- Methods in mixed-in modules: Modules can be mixed into a class using
prepend
,include
, orextend
. When this happens, the class has access to the methods defined in the modules, and Ruby goes into the modules to search for the method that has been called. It's also important to know that other modules can be mixed into the initial modules, and the search also progresses into these. - Instance methods: These are methods defined in the class and accessible by instances of that class.
- Parent class methods or modules: If the class happens to be a child of another class, Ruby searches in the parent class. The search goes into the parent class singleton methods, mixed modules, and its parent class.
- Object, Kernel, and BasicObject: These are the last places where Ruby searches. This is because every object in Ruby has these as part of their ancestors.
Classes and Modules
Methods are often called on objects. These objects are created by certain classes, which could be Ruby's inbuilt classes or classes created by a developer.
class Human
attr_reader :name
def initialize(name)
@name = name
end
def hello
put "Hello! #{name}"
end
end
We can then call the hello
method that we have created above on instances of the Human
class; for example,
john = Human.new("John")
john.hello # Output -> Hello John
The hello
method is an instance method; this is why we can call it on instances of the Human
class. There might be cases where we do not want the method to be called on instances. In these cases, we want to call the method on the class itself. To achieve this, we'll have to create a class method. Defining a class method for the class we have above will look like this:
def self.me
puts "I am a class method"
end
We can then call this by doing Human.me
. As the complexity of our application grows (imagine we're building a new start-up here), there might come a time when two or more of our classes have multiple methods that do the same thing. If this happens, it means we need to keep things dry and make sure that we do not repeat ourselves. The issue involves how we share functionality across these classes.
If you have not used modules before, you might be tempted to create a new class strictly for these "shared" methods. However, doing so might result in negative consequences, especially when you need to utilize multiple inheritance, something that Ruby does not support. Modules are the best means of handling this case. Modules are similar to classes, but they have a few differences. First, here is an example of what a module looks like:
module Movement
def walk
puts "I can walk!"
end
end
- Definition begins with the
module
keyword instead ofclass
. - Modules cannot have instances, so you cannot use
Movement.new
.
Methods
Methods can be viewed as actions to be performed by a particular object. If I have an array like [2, 3, 4]
assigned to a variable called numberList
, the .push
method is an action that can be performed by the array to put the value it receives into the array. This code snippet is an example:
john.walk
It might be typical of you to say something like, "I'm calling the object's method", in which john
references an object that is an instance of Human
, and walk
is the method. However, this isn't completely true because the inferred method tends to come from the object's class, superclass, or mixed-in module.
It is important to add that it's possible to define a method on an object, even an object like john
, because everything is an object in Ruby, even a class used in creating objects.
def john.drip
puts "My drip is eternal"
end
The drip
method can only be accessible by the object assigned to john
. drip
is a singleton method that will be available to the john
object. It is important to know that there's no difference between singleton methods and class methods, as you can see from this Stack Overflow answer. Unless you're referring to a method defined on an object like in the example above, it would be incorrect to say that the method belongs to a certain object. In our example, the walk
method belongs to the Movement
module, while the hello
method belongs to the Human
class. With this understanding, it will be easier to take this a step further, which is that to determine the exact method that is being called on an object, Ruby has to check the object's class or super class or modules that have been mixed in the object's hierarchy.
Mixing Modules
Ruby supports single inheritance only; a class can only inherit from one class. This makes it possible for the child class to inherit the behavior (methods) of another class. What happens when you have behaviors that need to be shared across different classes? For example, to make the walk
method available to instances of the Human
class, we can mix in the Movement
module in the Human
class. So, a rewrite of the Human
class using include
will look like this:
require "movement" # Assuming we have the module in a file called movement.rb
class Human
include Movement
attr_reader :name
def initialize(name)
@name = name
end
def hello
put "Hello! #{name}"
end
end
Now, we can call the walk
method on the instance:
john = Human.new("John")
john.walk
Include
When you make use of the include keyword, like we did above, the methods of the included module(s) get added to the class as instance method(s). This is because the included module is added among the ancestors of the Human
class, such that the Movement
module can be seen as a parent of the Human
class. As you can see in the example we shown above, we've called the walk
method on the instance of the Human
class.
Extend
In addition to include, Ruby provides us with the extend keyword. This makes the method(s) of the module(s) available to the class as class method(s), which are also known as singleton methods, as we learned previously. So, if we have a module called Feeding
that looks like
module Feeding
def food
"I make my food :)"
end
end
we can then share this behavior in our Human
class by requiring it and adding extend Feeding
. However, to use it, instead of calling the food
method on the instance of the class, we'll call it on the class itself, the same way we call class methods.
Human.food
Prepend
This is similar to include but with some differences, as stated in this post;
It actually works like include, except that instead of inserting the module between the class and its superclass in the chain, it will insert it at the bottom of the chain, even before the class itself.
What it means is that when calling a method on a class instance, Ruby will look into the module methods before looking into the class.
If we have a module that defines a hello
method that we then mix into the Human
class by using prepend
, Ruby will call the method we have in the module instead of the one we have in the class.
To properly understand how Ruby's prepend
works, I suggest taking a look at this article.
Method Lookup Path
The first place the Ruby interpreter looks when trying to call a method is the singleton methods. I created this repl, which you can play with to see the possible results.
Suppose we have a bunch of modules and classes that look like the following:
module One
def another
puts "From one module"
end
end
module Two
def another
puts "From two module"
end
end
module Three
def another
puts "From three module"
end
end
class Creature
def another
puts "From creature class"
end
end
Let's go ahead to mix these into the Human
class.
class Human < Creature
prepend Three
extend Two
include One
def another
puts "Instance method"
end
def self.another
puts "From Human class singleton"
end
end
Aside from mixing the modules, we have an instance and class method. You can also see that the Human
class is a subclass of the Creature
class.
First Lookup - Singleton Methods
When we run Human.another
, what gets printed is From Human class singleton
, which is what we have in the class method. If we comment out the class method and run it again, it will print From two module
to the console. This comes from the module we mixed in using extend
. It goes to show that the lookup begins among singleton methods. If we remove (or comment out) extend Two
and run the command again, this will throw a method missing error. We get this error because Ruby could not find the another
method among the singleton methods.
We'll go ahead and make use of the class instance by creating an instance:
n = Human.new
We'll also create a singleton method for the instance:
def n.another
puts "From n object"
end
Now, when we run n.another
, the version that gets called is the singleton method defined on the n
object. The reason Ruby won't look call the module mixed in using extend
in this case is because we're calling the method on the instance of the class. It is important to know that singleton methods have a higher relevance than methods involving modules mixed in using extend
.
Second Lookup - Modules Mixed In Using preprend
If we comment out the singleton method on the n
object and run the command, the version of the method that gets called is the module we mixed in using prepend
. This is because the use of prepend
inserts the module before the class itself.
Third Lookup - The Class
If we comment out the module Three
, the version of the another
method that gets called is the instance method defined on the class.
Fourth Lookup - Modules Mixed In Using include
The next place Ruby searches for the method is in modules that have been mixed in using include
. So, when we comment out the instance method, the version we get is that which is in module One
.
Fifth Lookup - Parent Class
If the class has a parent class, Ruby searches in the class. The search includes going into the modules mixed into the parent class; if we had the method defined in a module mixed into the Creature
class, the method will get called.
The End Of The Method Search
We can know where the search of a method ends by checking its ancestors: calling .ancestors
on the class. Doing this for the Human
class will return [Three, Human, One, Creature, Object, Kernel, BasicObject]
. The search for a method ends at the BasicObject
class, which is Ruby's root class. Every object that is an instance of some class originated from the BasicObject
class.
After the method search goes past the developer-defined parent class, it gets to the following:
- the
Object
class - the
Kernel
module - the
BasicObject
class
method_missing
Method
If you have been using Ruby for a while, you've probably come across NoMethodError
, which happens when you attempt to an unknown method on an object. This happens after Ruby has gone through the ancestors of the object and could not find the called method. The error message you receive is handled by the method_missing
method, defined in the BasicObject
class. It is possible to override the method for the object you're calling the method on, which you can learn about by checking this.
Conclusion
Now you know the path Ruby takes in figuring out the method called on an object. With this understanding, you should be able to easily fix errors arising as a result of calling an unknown method on an object.
About Honeybadger
Honeybadger has your back when it counts. We're the only error tracker that combines exception monitoring, uptime monitoring, and cron monitoring into a single, simple to use platform.
Our mission: to tame production and make you a better, more productive developer. Learn more
Posted on July 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024