Introduction

Every method call in Ruby is a search.

When you write obj.foo, Ruby doesn’t know yet where foo lives. It has to go looking: on the object, on its class, on the modules that class mixes in, on its superclass, and so on, until it finds a definition or runs out of places to look.

That search follows a fixed, inspectable order. Once you can see it, super, include, prepend, and class methods all turn out to be the same mechanism viewed from different angles.

Let’s make that order visible.

The ancestors chain

Ruby doesn’t improvise the search. Each class exposes the exact list it will walk, in order, through ancestors:

module Greetable
  def greet = "hi from Greetable"
end

class Animal
  include Greetable
end

class Dog < Animal
end

Dog.ancestors
# => [Dog, Animal, Greetable, Object, Kernel, BasicObject]

When you call a method on a Dog instance, Ruby walks this list from left to right and runs the first definition it finds:

Dog.new.greet
# => "hi from Greetable"

Dog has no greet. Neither does Animal. Greetable does, so the search stops there.

That’s the whole model. Everything else is just a rule about where a given thing lands in this list.

include: modules go just above the class

include inserts a module into the chain right after the class that includes it. Include several, and the order matters:

module A
  def who = "A"
end

module B
  def who = "B"
end

class C
  include A
  include B
end

C.ancestors
# => [C, B, A, Object, Kernel, BasicObject]

C.new.who
# => "B"

The last module included ends up closest to the class, so it wins. That surprises people who expect the first include to take precedence, but it’s consistent: each include slots the module just above C, pushing the previous one further away.

prepend: modules can go before the class

prepend is the mirror image. Instead of landing above the class, the module lands before it in the chain:

module Loud
  def who = super.upcase
end

class C
  prepend Loud

  def who = "c"
end

C.ancestors
# => [Loud, C, Object, Kernel, BasicObject]

C.new.who
# => "C"

Now Loud#who runs first, and its super calls C#who. This is the clean way to wrap or decorate an existing method (logging, memoization, instrumentation) without touching the original definition. Before prepend (Ruby 2.0), you had to resort to alias_method gymnastics for this.

super: keep walking the chain

super is how you jump to the next matching definition further down the chain, instead of stopping at the current one:

class Bicycle
  attr_reader :wheels, :seats

  def initialize
    @wheels = 2
    @seats = 1
  end
end

class Tricycle < Bicycle
  def initialize
    super
    @wheels = 3
  end
end

trike = Tricycle.new
trike.wheels # => 3
trike.seats  # => 1

Tricycle#initialize reuses everything its parent sets up, then overrides the one thing that differs. No duplication.

One subtlety worth knowing: bare super forwards the same arguments the current method received, while super() explicitly passes none. They are not interchangeable.

You can even inspect where super will land:

Tricycle.instance_method(:initialize).super_method
# => #<UnboundMethod: Bicycle#initialize()>

Singleton classes: methods on a single object

An object can carry methods that belong to it alone:

greeter = Object.new

def greeter.hello = "hello from the singleton"

greeter.hello
# => "hello from the singleton"

Where does hello live? Not on Object; no other object gained it. It lives on greeter’s singleton class, an invisible class Ruby inserts at the very front of the chain, ahead of the object’s real class:

greeter.singleton_class.ancestors
# => [#<Class:#<Object:0x...>>, Object, Kernel, BasicObject]

This is also what class methods really are. def self.foo defines foo on the class object’s singleton class — which is why extend gives you class-level methods: it mixes a module into the singleton class.

module Findable
  def find_all = "finding them all"
end

class User
  extend Findable
end

User.find_all
# => "finding them all"

include adds instance methods; extend adds methods to the receiver’s singleton class. Same insertion, different chain.

The root of it all

The chain always ends the same way:

Object.ancestors      # => [Object, Kernel, BasicObject]
BasicObject.ancestors # => [BasicObject]

Almost every object descends from Object, and Object mixes in Kernel, where puts, require, raise, and friends actually come from. They feel like keywords, but they’re just methods high up the chain.

The real root is BasicObject, a nearly empty class introduced in Ruby 1.9. It’s the blank slate you subclass when you want a proxy object with almost no inherited methods.

When nothing matches

If Ruby walks the entire chain without finding the method, it doesn’t just crash. It sends one more message: method_missing.

class Ghost
  def method_missing(name, *args)
    "you called #{name} with #{args.inspect}"
  end

  def respond_to_missing?(name, include_private = false)
    true
  end
end

Ghost.new.anything(1, 2)
# => "you called anything with [1, 2]"

method_missing itself is found through the normal chain: BasicObject defines the default one, and that default is what raises the familiar NoMethodError. Override it and you can answer calls dynamically. If you do, always define respond_to_missing? too, so respond_to? stays honest.

The one exception: refinements

Everything above walks the ancestors chain. Refinements are the single feature that doesn’t:

module StringExtensions
  refine String do
    def shout = "#{upcase}!"
  end
end

using StringExtensions

"hey".shout
# => "HEY!"

A refinement adds a method, but only lexically: it exists solely in the file or scope that activates it with using. Outside that scope, String has no shout. It’s a deliberate escape hatch from the global, chain-based model, meant for scoped monkey-patching without leaking changes everywhere.

Wrapping up

Method lookup in Ruby is not guesswork. It’s a walk down a list you can print:

  • ancestors is the map.
  • include slots a module above the class; the last one included wins.
  • prepend slots it before the class, so it can wrap the original via super.
  • super continues the walk instead of ending it.
  • singleton classes sit at the very front, where per-object and class methods live, and where extend mixes modules in.
  • the chain ends at Kernel and BasicObject, then method_missing.
  • refinements are the lone lexical exception.

Once you picture that chain, you can reason about inheritance and modules from a single list, instead of memorizing special cases.

Have comments or want to discuss this topic?

Send an email to ~bounga/public-inbox@lists.sr.ht