CrackedRuby logo

CrackedRuby

Method Lookup Chain

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