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:
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.
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