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.
Most Ruby developers lean on this every second without ever picturing it, and
that’s fine right up until it isn’t: a surprising super, a module that
overrides the wrong method, a class method that seems to appear from nowhere.
The good news is that the search follows a fixed, inspectable order you can
literally print. Once you can see it, super, include, prepend, and class
methods stop being separate rules to memorize and become one mechanism you can
reason about. It’s the mental model I lean on constantly and the first thing I
walk new Rubyists through.
Let me 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 really is the whole model, and it’s the one idea I make sure every Rubyist on my team internalizes early. Everything else in this post 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.
This is one I’ve watched trip up plenty of developers, because you’d expect
the first include to take precedence. It’s perfectly consistent once you
picture it, though: 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 my go-to
way to wrap or decorate an existing method (logging, memoization,
instrumentation) without touching the original definition. Before prepend
landed in Ruby 2.0, I used to reach for alias_method gymnastics to pull this
off, and I don’t miss them.
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. It’s the same insertion trick, just aimed at a 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 a walk down a list you can print, and I find that oddly reassuring:
ancestorsis the map.includeslots a module above the class; the last one included wins.prependslots it before the class, so it can wrap the original viasuper.supercontinues the walk instead of ending it.- singleton classes sit at the very front, where per-object and class
methods live, and where
extendmixes modules in. - the chain ends at
KernelandBasicObject, thenmethod_missing. - refinements are the lone lexical exception.
Picture that chain and inheritance and modules stop being a pile of special cases to memorize; they collapse into a single list you can read top to bottom. That’s the model I keep coming back to and the one I hand to anyone I mentor, because it turns “why on earth did that method resolve like that?” into a question you answer by reading a list.
Have comments or want to discuss this topic?
Send an email to ~bounga/bounga.org-discuss@lists.sr.ht