Overview
Ruby's Method Lookup Chain determines how the interpreter resolves method calls by searching through a specific sequence of classes and modules. When Ruby encounters a method call, it traverses this chain until it finds the method definition or exhausts all possibilities.
The lookup chain consists of the object's singleton class, included modules, the object's class, and the class's ancestors. Ruby searches these locations in a predetermined order, executing the first matching method it encounters.
class Animal
def speak
"Some sound"
end
end
class Dog < Animal
def speak
"Woof"
end
end
dog = Dog.new
dog.speak # => "Woof"
Ruby searches the Dog class first, finds the speak
method, and executes it without checking the Animal class. This demonstrates the fundamental principle that Ruby stops searching once it locates a matching method.
The Method Lookup Chain becomes complex when modules enter the picture. Ruby inserts included modules into the chain above the including class, while prepended modules go below the class in the hierarchy.
module Friendly
def speak
"Hello! #{super}"
end
end
class Dog < Animal
include Friendly
def speak
"Woof"
end
end
dog = Dog.new
dog.speak # => "Hello! Woof"
Basic Usage
Understanding the method lookup order requires examining Ruby's ancestors
method, which returns the complete chain Ruby searches when resolving method calls.
class Vehicle
def start
"Engine starting"
end
end
module GPS
def navigate
"GPS navigating"
end
end
class Car < Vehicle
include GPS
def start
"Car engine starting"
end
end
puts Car.ancestors
# => [Car, GPS, Vehicle, Object, Kernel, BasicObject]
Ruby searches this chain from left to right. When calling Car.new.start
, Ruby checks Car first, finds the method, and executes it. The Vehicle class's start
method never gets called because Ruby found a match earlier in the chain.
Module inclusion affects the lookup chain by inserting the module above the including class. Multiple module inclusions stack in reverse order of inclusion.
module A
def test; "A"; end
end
module B
def test; "B"; end
end
class Example
include A
include B
end
puts Example.ancestors
# => [Example, B, A, Object, Kernel, BasicObject]
Example.new.test # => "B"
Module B appears first in the ancestors chain because it was included last, demonstrating Ruby's last-included-first-searched behavior for modules.
Prepending modules changes the lookup order by placing the module before the class in the chain, allowing modules to intercept method calls before the class handles them.
module Logging
def save
puts "Logging save operation"
super
end
end
class Document
prepend Logging
def save
puts "Saving document"
end
end
puts Document.ancestors
# => [Logging, Document, Object, Kernel, BasicObject]
Document.new.save
# Logging save operation
# Saving document
Singleton methods create a special anonymous class in the lookup chain, positioned immediately after the object itself. This singleton class takes precedence over all other method definitions.
class Person
def greet
"Hello"
end
end
person = Person.new
def person.greet
"Special greeting"
end
person.greet # => "Special greeting"
# The singleton class appears in the chain
puts person.singleton_class.ancestors
# => [#<Class:#<Person:0x...>>, Person, Object, Kernel, BasicObject]
Advanced Usage
Complex inheritance hierarchies with multiple levels of modules and classes create intricate lookup chains. Ruby maintains consistent resolution rules regardless of complexity.
module Trackable
def track
"Tracking from #{self.class}"
end
end
module Cacheable
def cache_key
"#{self.class.name.downcase}:#{object_id}"
end
end
class BaseModel
include Trackable
def save
track
"BaseModel save"
end
end
module Validatable
def save
return "Validation failed" unless valid?
super
end
def valid?
true
end
end
class User < BaseModel
prepend Validatable
include Cacheable
def save
cache_key
"User save: #{super}"
end
end
puts User.ancestors
# => [Validatable, User, Cacheable, BaseModel, Trackable, Object, Kernel, BasicObject]
user = User.new
user.save
# => "User save: BaseModel save"
The method resolution demonstrates how prepended modules intercept calls, included modules provide additional functionality, and super
navigates through the chain. Validatable executes first due to prepending, calls super
to reach User's save
method, which calls super
again to reach BaseModel.
Method aliasing creates multiple entry points into the lookup chain, with each alias following the same resolution rules as the original method name.
module StringExtensions
def reverse_case
chars.map { |c| c == c.upcase ? c.downcase : c.upcase }.join
end
alias flip_case reverse_case
end
class CustomString < String
include StringExtensions
def reverse_case
"Custom: #{super}"
end
end
str = CustomString.new("Hello")
str.reverse_case # => "Custom: hELLO"
str.flip_case # => "Custom: hELLO"
Both method names follow identical lookup paths, with the alias accessing the same method definition and inheritance chain.
Dynamic method definition affects the lookup chain based on where Ruby defines the method. Methods defined in singleton classes take precedence over class methods, while methods added to modules affect all including classes.
module DynamicMethods
def self.included(base)
base.define_method(:dynamic_method) do
"Dynamically defined in #{self.class}"
end
end
end
class Example
include DynamicMethods
# Later, add a singleton method
define_singleton_method(:dynamic_method) do
"Singleton method"
end
end
Example.new.dynamic_method # => "Dynamically defined in Example"
Example.dynamic_method # => "Singleton method"
Module refinements create localized changes to the method lookup chain, affecting method resolution only within the refined scope.
module StringRefinements
refine String do
def palindrome?
self == reverse
end
end
end
class PalindromeChecker
using StringRefinements
def check(str)
str.palindrome?
end
end
# Outside the refined scope, method doesn't exist
begin
"racecar".palindrome?
rescue NoMethodError
puts "Method not available outside refinement"
end
checker = PalindromeChecker.new
checker.check("racecar") # => true
Common Pitfalls
Method name conflicts between included modules create unexpected behavior when developers assume method resolution follows inclusion order rather than Ruby's actual lookup rules.
module DatabaseLogger
def log(message)
puts "[DB] #{message}"
end
end
module FileLogger
def log(message)
puts "[FILE] #{message}"
end
end
class Service
include DatabaseLogger
include FileLogger # This takes precedence
end
Service.new.log("Error occurred")
# => [FILE] Error occurred
# Not [DB] Error occurred as might be expected
The last included module wins, not the first. Many developers expect the first included module to take precedence, leading to confusion when FileLogger's method executes instead of DatabaseLogger's method.
Super calls without corresponding methods in the ancestor chain raise NoMethodError exceptions, often occurring when developers remove or rename methods in parent classes.
module Mixin
def process
super # Assumes parent class has process method
end
end
class Worker
include Mixin
# No process method defined
end
begin
Worker.new.process
rescue NoMethodError => e
puts e.message # => super: no superclass method `process'
end
This error occurs because super
looks for a method higher in the chain, but no such method exists. The solution requires either defining the method in a parent class or using super
conditionally.
Module ordering dependencies create fragile code when modules assume specific positions in the lookup chain or expect other modules to be present.
module Authentication
def authenticate
return false unless authorized? # Assumes Authorization module
true
end
end
module Authorization
def authorized?
true
end
end
class SecureController
include Authentication
# Missing Authorization include
end
begin
SecureController.new.authenticate
rescue NoMethodError => e
puts e.message # => undefined method `authorized?'
end
Authentication module depends on Authorization module's methods, but the dependency isn't explicit. Including Authentication without Authorization breaks functionality.
Singleton method shadowing obscures class and module methods, making code behavior unpredictable when developers aren't aware of singleton method definitions.
class Calculator
def add(a, b)
a + b
end
end
calc = Calculator.new
# Someone adds a singleton method
def calc.add(a, b)
a * b # Wrong operation!
end
result = calc.add(2, 3) # => 6, not 5
puts "Expected 5, got #{result}"
The singleton method completely shadows the class method, changing the object's behavior without any indication at the call site. This creates debugging challenges when singleton methods are added dynamically or in distant parts of the codebase.
Method redefinition warnings get suppressed in many Ruby environments, hiding cases where modules or classes accidentally override existing methods.
class Array
def size # Redefining built-in method
"Custom size implementation"
end
end
arr = [1, 2, 3]
arr.size # => "Custom size implementation"
arr.length # => 3 (still works)
Redefining core methods breaks existing functionality and creates inconsistent behavior. The warning system helps identify these issues, but many production environments suppress warnings.
Reference
Method Lookup Chain Order
Position | Location | Description |
---|---|---|
1 | Singleton class | Object-specific methods defined with def obj.method |
2 | Prepended modules | Modules added with prepend , in reverse inclusion order |
3 | Class | The object's immediate class |
4 | Included modules | Modules added with include , in reverse inclusion order |
5 | Superclass | Parent class and its included modules |
6 | Object | Ruby's base Object class |
7 | Kernel | Module included in Object |
8 | BasicObject | Root of Ruby's class hierarchy |
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#ancestors |
None | Array<Class, Module> |
Returns method lookup chain for class or module |
#singleton_class |
None | Class |
Returns object's singleton class |
#singleton_methods |
all=true (Boolean) |
Array<Symbol> |
Lists singleton methods defined on object |
#method |
name (Symbol/String) |
Method |
Returns method object for given name |
#respond_to? |
method_name (Symbol/String), include_private=false (Boolean) |
Boolean |
Checks if object responds to method |
#send |
method_name (Symbol/String), *args |
Object |
Calls method by name |
#super |
*args |
Object |
Calls method with same name from superclass |
Module Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#include |
*modules |
self |
Adds modules above class in lookup chain |
#prepend |
*modules |
self |
Adds modules below class in lookup chain |
#extend |
*modules |
self |
Adds modules to singleton class |
#included_modules |
None | Array<Module> |
Returns included modules |
#prepended_modules |
None | Array<Module> |
Returns prepended modules |
Method Visibility
Visibility | Access Rules | Lookup Behavior |
---|---|---|
public |
Callable from anywhere | Normal lookup chain traversal |
protected |
Callable from same class/subclass | Follows inheritance chain |
private |
Callable without explicit receiver | Searches current object only |
Common Exceptions
Exception | Cause | Resolution |
---|---|---|
NoMethodError |
Method not found in lookup chain | Define method or check method name |
ArgumentError |
Wrong number of arguments | Check method signature |
NameError |
Constant not found | Check constant definition and scope |
Refinement Scope
Context | Refinement Active | Method Resolution |
---|---|---|
Inside using block |
Yes | Refined methods take precedence |
Outside refinement scope | No | Original methods used |
Nested refinements | Yes | Last using takes precedence |
Performance Characteristics
Operation | Time Complexity | Notes |
---|---|---|
Method call (cached) | O(1) | Ruby caches method lookups |
Method call (uncached) | O(n) | Where n is ancestors chain length |
ancestors call |
O(n) | Builds array of all ancestors |
Singleton method definition | O(1) | Creates singleton class if needed |