Understanding Ruby Method Lookup

Ruby lets you express yourself like few other languages, with a minimum of boilerplate. It's fantastic until it isn't. Until one day when you think you're calling the `foo` method you wrote last week, but instead, you end up calling a `foo` method that came with some gem from 2008. In these situations, knowing about Ruby's method lookup rules will save your bacon.

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:

  1. Where the method .valid? is defined.
  2. 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:

  1. 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.
  2. Methods in mixed-in modules: Modules can be mixed into a class using prepend, include, or extend. 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.
  3. Instance methods: These are methods defined in the class and accessible by instances of that class.
  4. 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.
  5. 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
  1. Definition begins with the module keyword instead of class.
  2. 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.

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.

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

author photo

Kingsley Silas

Kingsley works as a software engineer and enjoys writing technical articles.


“We’ve looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.”
Michael Smith
Try Error Monitoring Free for 15 Days
Are you using Bugsnag, Rollbar, or Airbrake for your monitoring? Honeybadger includes exception, uptime, and check-in monitoring — all for probably less than you’re paying now. Discover why so many companies are switching to Honeybadger here.
Try Error Monitoring Free for 15 Days
Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
Try Error Monitoring Free for 15 Days
"Wow — Customers are blown away that I email them so quickly after an error."
Chris Patton
Try Error Monitoring Free for 15 Days