Method Lookup and super
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them.
Every method call in Ruby asks one question: starting from the receiver, which method object has this name? The answer is
decided by a single, linear list of modules and classes, the ancestor chain, searched from the front. Everything else on this
page follows from that list: what include, prepend and extend really do, why super can work inside a module that has no
superclass, how a Method object can tell you where a definition lives, what happens when nothing is found and method_missing gets its turn,
and why wrapping a method with prepend is safer than the old alias chain.
The page builds on Classes and Objects,
Modules and Mixins and
Singleton Classes and Class-Level Behavior, and
repeats the signature rules of Methods and Parameters only where
super needs them. Visibility checks happen after a method is found and are in
Visibility and Encapsulation; refined methods are consulted
before this chain and are in Refinements and Monkey Patching;
generating methods and calling them by name is in
Dynamic Dispatch and Method Definition, the hooks that fire
when the chain changes are in Hooks, eval and DSLs, and the reflection API that
inspects it is in Reflection, Introspection and Tracing. The
exceptions raised by a failed lookup are catalogued in
Exceptions and Error Handling. For comparison, Python resolves
methods with the C3 linearisation described in Python classes, and Java resolves them
through single inheritance plus default interface methods (see Java inheritance).
Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the
configuration of the running shelf gem (target Ruby 4.0, double-quoted strings, NewCops: enable, Style/Documentation and
Style/OneClassPerFile disabled because each example is a self-contained file with several small classes). The output shown is the real
output. Files that use the shelf library are run with ruby -Ilib, so require "shelf" finds it (see the shelf gem source); the others are standalone.
The lookup path
The ancestor chain
When Ruby evaluates receiver.name, it takes the singleton class of the receiver, which is where the search starts, and walks its
ancestors in order until a method called name is found. Module#ancestors returns that list, and for ordinary (non-singleton)
classes the list is built from four ingredients, in this order:
-
The modules that the class prepends, the most recently prepended first.
-
The class itself.
-
The modules that the class includes, the most recently included first.
-
The superclass, and then the same three steps for it, up to
BasicObject.
Before all of that comes the singleton class of the receiver itself, with the modules added by extend. A method defined with
def object.name or inside class << object is found first, because it is the first entry of the chain. Each entry of the chain is
visited once, and a module that appears twice (included by a class and by its superclass, or included twice) is kept only at its first
position in the chain.
The next file builds a class with all five sources of methods and asks the objects where each answer came from. Audited is
prepended, Titled is included, Item is the superclass, and a singleton method is defined on a single instance. The final lines
show that the same view applies to a class from the shelf gem.
# frozen_string_literal: true
require "shelf"
# One object, five places a method can come from.
module Audited
def to_s = "[audited] #{super}"
end
module Titled
def to_s = "titled(#{super})"
def label = "label from Titled"
end
class Item
def to_s = "an item"
def label = "label from Item"
def weight = 1
end
class Shelved < Item
include Titled
prepend Audited
def to_s = "shelved>#{super}"
end
copy = Shelved.new
# a singleton method on one object is found first
def copy.weight = 2
puts copy
puts copy.label
puts copy.weight
p Shelved.ancestors.first(5)
p copy.singleton_class.ancestors.drop(1).first(4)
p copy.method(:to_s).owner
p copy.method(:label).owner
p copy.method(:weight).owner.singleton_class?
p Shelf::Catalog.instance_method(:each).owner
p Shelf::Catalog.ancestors
Output:
[audited] shelved>titled(an item)
label from Titled
2
[Audited, Shelved, Titled, Item, Object]
[Audited, Shelved, Titled, Item]
Audited
Titled
true
Shelf::Catalog
[Shelf::Catalog, Enumerable, Object, Kernel, BasicObject]
Source: Module#ancestors, Method#owner and Modules and Classes
Reading the output: to_s is found in Audited before the class Shelved is even looked at, and Audited#to_s calls super, which
continues from the entry after Audited. label is not defined by Shelved, so the search passes the class and finds the included
Titled before reaching Item, which also defines it. weight is found in the singleton class of that one object, and
singleton_class? confirms it. Shelf::Catalog#each has Shelf::Catalog as its owner, and Enumerable sits between the class and Object
because Catalog includes it. Kernel is the only module between Object and BasicObject.
The chain is a flat list, so there is no "diamond problem" to resolve: a method is found at the first position that defines it, and
super always means "the next position". Which position a module takes is decided only by whether it was included, prepended or
extended, and by the order of those calls.
| Mechanism | Where the module goes | Typical use |
|---|---|---|
|
Directly after the class, before the superclass; the last |
Adding behaviour that the class may override ( |
|
Directly before the class; the last |
Wrapping or decorating methods the class defines itself; instrumentation. |
|
Directly after the singleton class of that object (it is |
Adding methods to one object or to a class as class methods. |
|
The singleton class itself, the first entry. |
Per-object methods, class methods. |
|
The new class is placed before |
Inheritance. |
include, prepend and extend in detail
include inserts a module after the class; with several arguments the first one named is searched first, so include Loggable, Cacheable
puts Loggable ahead of Cacheable (the arguments are processed right to left, and the last processed is searched first).
Including a module that is already in the chain does nothing. prepend inserts a module before the class, and has the same
rule about repeated calls. The hooks included, prepended and extended, and the methods they are called by (append_features,
prepend_features, extend_object), are covered in
Hooks, eval and DSLs.
# frozen_string_literal: true
module Loggable; end
module Cacheable; end
module Late; end
Base = Class.new
class Plain < Base
end
Plain.include(Loggable, Cacheable) # Loggable is searched first
p Plain.ancestors.first(4)
class Again < Base
include Cacheable
include Loggable
include Cacheable # already in the chain: no effect
end
p Again.ancestors.first(4)
# Since Ruby 3.0 a module changed after inclusion is seen by its includers.
class Host < Base
include Loggable
end
Loggable.include(Cacheable)
Loggable.prepend(Late)
p Host.ancestors.first(5)
# prepend puts the module in front of the class itself
module Wrapper; end
class Front < Base
prepend Wrapper
include Cacheable
end
p Front.ancestors.first(4)
p Front.include?(Wrapper), Front.ancestors.index(Wrapper) < Front.ancestors.index(Front)
Output:
[Plain, Loggable, Cacheable, Base]
[Again, Loggable, Cacheable, Base]
[Host, Late, Loggable, Cacheable, Base]
[Wrapper, Front, Cacheable, Base]
true
true
Source: Module#include, Module#prepend and Ruby 3.0.0 NEWS
Four details are worth remembering. First, Plain.include(Loggable, Cacheable) puts Loggable first, and so do two separate statements in
the opposite order (include Cacheable and then include Loggable): the module included last is searched first. Second, repeating include Cacheable in Again changes nothing, because the module is
already in the chain. Third, since Ruby 3.0 a change made to a module after a class included it is visible to that class:
Loggable.include(Cacheable) and Loggable.prepend(Late) both reached Host, which had included Loggable earlier. Before 3.0
only classes that included the module afterwards saw the change, which is why older tutorials say "include modules into a module before
including it anywhere". Fourth, a class that prepends a module also gets the modules that module includes, in front of itself,
which is why the example uses a fresh Wrapper module for Front.
prepend has one more property: it is the only built-in way to put code in front of a method that the same class defines, because the
prepended module is searched before the class. That is what makes the wrapping technique in the section below possible.
Class methods follow the same rules
A class method is a singleton method of the class object, so it is looked up in the singleton class of the class. The singleton class of a
subclass inherits from the singleton class of its superclass, which is why class methods are inherited, and extend on a class adds
a module to that singleton chain. super inside a class method therefore continues along the singleton chain.
# frozen_string_literal: true
# Class methods are singleton methods, so they follow the singleton-class chain.
class Repository
def self.build(*) = new(*)
def self.table = "repositories"
def self.inherited(subclass)
super
subclass.instance_variable_set(:@registered, true)
end
def initialize(name = "default") = @name = name
attr_reader :name
end
class BookRepository < Repository
def self.table = "books:#{super}"
end
module Prefixed
def table = "app_#{super}"
end
BookRepository.extend(Prefixed)
p BookRepository.table
p BookRepository.build("main").name
p BookRepository.singleton_class.ancestors.first(4)
p BookRepository.method(:build).owner
p BookRepository.method(:table).owner
p BookRepository.method(:table).super_method.owner
p BookRepository.singleton_class.superclass
p BookRepository.instance_variable_get(:@registered)
# Instances do not see class methods: they come from the class, not its singleton class
begin
BookRepository.new.table
rescue NoMethodError => e
puts e.message
end
Output:
"books:app_repositories"
"main"
[#<Class:BookRepository>, Prefixed, #<Class:Repository>, #<Class:Object>]
#<Class:Repository>
#<Class:BookRepository>
Prefixed
#<Class:Repository>
true
undefined method 'table' for an instance of BookRepository
Source: Class, Object#extend and Method#super_method
BookRepository.table runs BookRepository.table first (its own singleton method), whose super reaches Prefixed#table (the extended
module) and whose super reaches Repository.table. BookRepository.singleton_class.superclass is #<Class:Repository>, which proves that the
singleton classes mirror the class hierarchy, and BookRepository.new.table fails because instances start at BookRepository, not at its
singleton class. The inherited hook in the example calls super first, the rule for every hook that other modules may also define. The
structure of singleton classes is explained in
Singleton Classes and Class-Level Behavior.
Where refinements and the cache fit
Active refinements are consulted before the chain described above: for a class C Ruby checks the refinements of C (most recently
activated first), then the prepended modules of C, C, and its included modules, and only then repeats the search for the superclass. A method in
a subclass beats a refinement of its superclass, which surprises people who refine Numeric and then call Integer#/. See
Refinements and Monkey Patching and the
refinements syntax documentation.
The result of a lookup is cached by the virtual machine, and the cache is invalidated when a method is defined, removed, aliased, or when the chain changes. That is an implementation detail and not a behaviour you can rely on, but it explains why defining methods in a hot loop, or including modules at run time in a tight request path, is expensive. The page Performance, Benchmarking and Profiling returns to this.
Finding out where a method comes from
Reading the chain by eye does not scale, so Ruby exposes it through Method objects. Object#method(:name) returns the Method that
would run (it raises NameError when there is none), and Module#instance_method(:name) returns an UnboundMethod for a class or
module. Both answer owner (the module or class whose body holds the definition), name and original_name (differ for aliases),
arity, parameters, source_location (a file and line, nil for methods written in C) and super_method, the method that super would reach. Following
super_method until it returns nil lists every definition of that name in the chain.
# frozen_string_literal: true
require "shelf"
# Method objects answer "which method will run, and where is it defined?"
module Shouting
def to_s = super.upcase
end
class Reader < Shelf::Catalog
include Shouting
def to_s = "reader>#{super}"
end
reader = Reader.new
method = reader.method(:to_s)
chain = []
while method
chain << [method.owner, method.source_location&.last]
method = method.super_method
end
chain.each { |owner, line| puts "#{owner} #{line.inspect}" }
m = reader.method(:each)
p m.owner, m.receiver.equal?(reader), m.name, m.arity
p m.unbind.class
unbound = Shelf::Catalog.instance_method(:each)
p unbound.bind(reader).call.class
p unbound.bind_call(reader).class
begin
unbound.bind(Object.new)
rescue TypeError => e
puts e.message
end
p Reader.instance_method(:to_s).owner
p Reader.public_instance_method(:to_s).super_method&.owner
p Reader.method_defined?(:to_s, false), Reader.method_defined?(:each, false), Reader.method_defined?(:each)
p Reader.instance_methods(false)
Source: Method, UnboundMethod and Module#public_instance_method
Output:
Reader 13
Shouting 7
Kernel nil
Shelf::Catalog
true
:each
0
UnboundMethod
Enumerator
Enumerator
bind argument must be an instance of Shelf::Catalog
Reader
Shouting
true
false
true
[:to_s]
The first loop walks to_s through Reader (line 13), Shouting (line 7) and the C implementation in Kernel (no source location).
The second block shows the Method of each: its owner is Shelf::Catalog even though it was called on a Reader, and unbind returns an
UnboundMethod that must be bound to an instance of the owner (or a subclass, or, for a module method, any object): binding to an unrelated object raises
TypeError. bind_call binds and calls in one step without allocating an intermediate Method, the usual way to call a method of a specific module
or class regardless of overrides. The final lines show that method_defined? and instance_methods can be limited to the class itself
with false.
The other tools worth knowing are:
| Tool | What it answers |
|---|---|
|
The full chain of a class or module. |
|
The chain including the singleton class and extended modules. |
|
Whether a module is in the chain, and how two modules relate ( |
|
The method that would run; |
|
Where it is defined, whose it is, the next definition, and its file and line. |
|
Whether the public or protected method exists, and the list of them; |
|
The names blocked in this class by |
|
Whether a call would succeed, honouring |
|
The parent, direct children, and lexical nesting (the last is about constants, not methods). |
Source: Module, Object#method and Object#respond_to?
How super finds the next method
super calls the method of the same name that comes after the current method’s owner in the ancestor chain of the receiver. Two
things follow. The "current" position is the module or class whose body contains the def, not the class of self. And the "chain" is
the one of self, not the one of the module being defined, so the next method depends on who includes the module.
The forms of super, with their argument rules, are in the section on
the forms of super; in short, bare super passes the current values of the
method’s parameters (including keywords and the block), super() passes no arguments, and super(a, k: 1) passes exactly what is written. In all
three the block of the current call goes along unless you write &nil.
| Form | Arguments passed | Block passed |
|---|---|---|
|
The current values of every parameter of the method, including optional, rest, keyword and keyword-rest parameters. |
The current block. |
|
None. |
The current block. |
|
Exactly what is written. |
The current block. |
|
Exactly what is written. |
The block |
|
The arguments captured by a |
Included in |
The next file shows a cooperative chain. Trace is prepended, Strict is included and Job is the superclass, and each layer calls super
once, so every layer runs and the innermost one yields to the block. The same Greeting module is then mixed into two unrelated classes,
and its super reaches a different method in each. defined?(super) lets a module work both with and without a successor, and &nil withholds the block.
# frozen_string_literal: true
# A cooperative chain: every layer calls super and passes the block on.
module Trace
def run
["Trace", *super]
end
end
module Strict
def run
["Strict", *super]
end
end
class Job
def run
[yield]
end
end
class Email < Job
include Strict
prepend Trace
def run
["Email", *super]
end
end
result = Email.new.run { "block" }
p result
p Email.ancestors.first(4)
# The same module is spliced into different chains, so its super differs.
module Greeting
def greet = "hello, #{super}"
end
class Visitor
def greet = "visitor"
end
class Member
def greet = "member"
end
class VisitorGreeter < Visitor
include Greeting
end
class MemberGreeter < Member
include Greeting
end
p VisitorGreeter.new.greet
p MemberGreeter.new.greet
# defined?(super) guards the end of a chain, &nil withholds the block
module Optional
def describe = defined?(super) ? "optional+#{super}" : "optional"
end
class Plain
include Optional
end
class WithParent
def describe = "parent"
end
class Derived < WithParent
include Optional
end
p Plain.new.describe
p Derived.new.describe
class Eager
def each_item(count = 1) = block_given? ? yield(count) : :no_block
end
class Quiet < Eager
def each_item(count = 2, &) = super(count.succ, &nil)
end
p(Eager.new.each_item { |x| x * 10 })
p(Quiet.new.each_item { |x| x * 10 })
Output:
["Trace", "Email", "Strict", "block"]
[Trace, Email, Strict, Job]
"hello, visitor"
"hello, member"
"optional"
"optional+parent"
10
:no_block
Source: Ruby 4.0 documentation: Methods and defined?
The order ["Trace", "Email", "Strict", "block"] is the ancestor chain [Trace, Email, Strict, Job] read from left to right, with the block
reaching the innermost Job#run, which proves that implicit block passing survives all three hops. In the second part the output is "hello, visitor"
for one class and "hello, member" for the other. That is how a mixin can add behaviour around whatever method comes next without knowing it,
the idea behind Comparable-style protocols, middleware stacks and the decorator pattern discussed in
SOLID and Design Patterns.
In the last part, Quiet#each_item calls super(count.succ, &nil) so that Eager#each_item never sees a block and returns :no_block, while the
unmodified call yields 10. Without &nil the block would have been passed on silently.
Rules for super
-
superworks in instance methods, singleton methods and class methods, and inside blocks within them, where it refers to the enclosing method. -
It is searched from the owner of the method being executed. Aliasing does not change that: an aliased copy of
showthat callssuperstill searches forshow, not for the alias name (demonstrated below). -
Without a successor,
superends inmethod_missing, whose default raisesNoMethodError("super: no superclass method"). A class that definesmethod_missingcan therefore intercept the call. -
Inside
define_method, baresuperis not allowed (the block has no named parameters to forward) and raisesRuntimeError; write the arguments out. -
superininitializereachesBasicObject#initialize, which takes no arguments: a class that inherits directly fromObjectand calls baresuperwith parameters, orsuper(x), fails withArgumentError. Usesuper(). -
Methods that use
superin a module and are called on a class that has no successor needdefined?(super)or a safe default. -
Hooks (
inherited,included,extended,prepended,method_added) must callsuperso that other modules in the chain keep working. -
Data.defineandStruct.newinstances usesuperininitializeto set fields: see Struct, Data and Value Objects.
# frozen_string_literal: true
class Root
def lend(book) = "lent #{book}"
end
class NoParent
def lend(book) = super(book.strip)
end
class Defined < Root
define_method(:lend) { |book| super(book.upcase) }
end
class Implicit < Root
define_method(:lend) { |_book| super }
end
class Fixed < Root
# BasicObject#initialize takes no arguments, so super() is correct
def initialize(title)
@title = title
super()
end
end
class Forwarded
# a bare super passes title on to BasicObject#initialize
def initialize(title)
@title = title
super
end
end
def attempt
yield
rescue StandardError => e
puts "#{e.class}: #{e.message}"
end
attempt { NoParent.new.lend("Dune") }
attempt { puts Defined.new.lend("Dune") }
attempt { Implicit.new.lend("Dune") }
attempt { puts Fixed.new("Dune").class }
attempt { Forwarded.new("Dune") }
Output:
NoMethodError: super: no superclass method 'lend' for an instance of NoParent
lent DUNE
RuntimeError: implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly.
Fixed
ArgumentError: wrong number of arguments (given 1, expected 0)
Source: Ruby 4.0 documentation: Methods and BasicObject
The five calls give five different outcomes. NoParent#lend calls super but no ancestor defines lend, so Ruby raises NoMethodError with the receiver’s class in the
message. Defined calls super(book.upcase) with explicit arguments and works. Implicit uses bare super inside define_method and raises RuntimeError, whose
message tells you to specify the arguments. Fixed calls super() in initialize, the idiom for classes whose parent takes no arguments, and Forwarded passes its
parameter on implicitly to BasicObject#initialize, which raises ArgumentError. The same failure appears when a class calls
bare super from initialize while inheriting from a parent that takes no arguments, a very common cause of "wrong number of arguments" in subclasses.
When nothing is found
method_missing
If the whole chain is searched without a match, Ruby calls method_missing(name, *args, &block) on the receiver, which starts another
lookup (for method_missing itself). The default implementation, BasicObject#method_missing, is private and raises: NoMethodError when
the call had an explicit receiver (or self.), and NameError ("undefined local variable or method") when the name
looked like a variable, that is, a bare identifier with no arguments and no receiver. The exception carries the facts: NameError#name,
NameError#receiver, and for NoMethodError also args.
Overriding method_missing gives ghost methods: calls that do not correspond to any def. The rules that keep them from becoming a
liability are:
-
Handle only names you recognise, and call
superfor everything else, so that real errors are still reported (RuboCop’sLint/MissingSuperandStyle/MissingRespondToMissingcheck this). -
Define
respond_to_missing?(name, include_private = false)with the same condition and end it with|| super. Thenrespond_to?andmethodagree with the ghost methods, so duck-typing checks work. -
Forward the whole call:
*args, keywords and the block (anonymous*,**and¶meters can be forwarded since Ruby 3.2). -
Prefer defining real methods when the set of names is known; ghost methods are slower than real ones because every call walks the whole chain first. The
define_methodapproach is covered in Dynamic Dispatch and Method Definition.
# frozen_string_literal: true
require "shelf"
# Dynamic finders on top of the catalogue: ghost methods done properly.
class FindableCatalog < Shelf::Catalog
FINDER = /\Afind_by_(?<field>title|author)\z/
def method_missing(name, *args, &)
match = FINDER.match(name)
return super unless match
field = match[:field]
find { |book| field == "title" ? book.title == args.first : book.author.name == args.first }
end
def respond_to_missing?(name, include_private = false)
FINDER.match?(name) || super
end
end
isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Dune", author: Shelf::Author.new(name: "Frank Herbert"))
catalog = FindableCatalog.new(store: Shelf::Store::Memory.new).add(book)
puts catalog.find_by_title("Dune")
puts catalog.find_by_author("Frank Herbert")
p catalog.respond_to?(:find_by_title)
p catalog.respond_to?(:find_by_isbn)
p catalog.method(:find_by_author).call("Nobody")
begin
catalog.find_by_isbn("x")
rescue NoMethodError => e
puts e.message
p e.name, e.args, e.receiver.equal?(catalog)
end
Output:
Dune by Frank Herbert
Dune by Frank Herbert
true
false
nil
undefined method 'find_by_isbn' for an instance of FindableCatalog
:find_by_isbn
["x"]
true
find_by_title and find_by_author do not exist as methods, yet they work, and respond_to? answers true for them and false for find_by_isbn. catalog.method(:find_by_author)
returns a Method object because respond_to_missing? vouches for the name; calling it goes through method_missing and returns nil, since no author matches. The unknown
find_by_isbn reaches super, which raises the standard NoMethodError, with the name, arguments and receiver intact. If the example omitted
return super unless match, that call would silently return nil and hide the typo.
BasicObject proxies
BasicObject is the root of the hierarchy and defines almost nothing: ==, equal?, !, !=, instance_eval, instance_exec, __send__, __id__, and the private
initialize, method_missing, singleton_method_added, singleton_method_removed and singleton_method_undefined. Kernel (puts, raise,
inspect, class, respond_to?, is_a?) comes in through Object, so a class that inherits from BasicObject has none of it, and
references to top-level constants need ::. Such a class is the base for transparent proxies, where every method call must be forwarded and none should be answered by the proxy.
# frozen_string_literal: true
# BasicObject sits above Kernel: a proxy inherits almost nothing, so everything is forwarded.
class Recorder < BasicObject
def initialize(target)
@target = target
@calls = []
end
attr_reader :calls
def method_missing(name, *, &)
return super unless @target.respond_to?(name)
@calls << name
@target.public_send(name, *, &)
end
def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private)
end
recorder = Recorder.new([3, 1, 2])
p recorder.sort
p recorder.first
p recorder.calls
begin
recorder.shuffle!(1, 2, 3)
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
begin
recorder.nothing_like_this
rescue NoMethodError => e
puts e.message
end
# Kernel methods are not reachable from inside a BasicObject subclass
class Blind < BasicObject
def try_puts = puts("hi")
end
begin
Blind.new.try_puts
rescue NameError => e
puts "#{e.class}: #{e.message}"
end
Output:
[1, 2, 3]
3
[:sort, :first]
ArgumentError: wrong number of arguments (given 3, expected 0)
undefined method 'nothing_like_this' for an instance of Recorder
NoMethodError: undefined method 'puts' for an instance of Blind
Source: BasicObject and Kernel
sort and first reach the target through method_missing and are recorded. shuffle!(1, 2, 3) is forwarded as is, and the target raises its own
ArgumentError. A name the target does not know goes to super and gives NoMethodError ("for an instance of Recorder"). Blind shows the cost of
BasicObject: even puts is a Kernel method and is not found.
Error messages
The text of the lookup errors changed twice, and the messages that older tutorials and Stack Overflow answers quote no longer match what
Ruby 4.0 prints. Since Ruby 3.3 the receiver is described by its class instead of by inspect, and since Ruby 3.4 the method name is quoted with
apostrophes instead of a backtick and an apostrophe, and a class name prefixes the method name in backtraces.
| Situation | Ruby 4.0 message | Exception |
|---|---|---|
Unknown method on an object |
|
|
Unknown method on |
|
|
Unknown class method |
|
|
Bare identifier that is neither a variable nor a method |
|
|
Private method called with a receiver |
|
|
Protected method called from outside |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Alias chain installed twice |
|
|
Source: Ruby 3.3.0 NEWS and Ruby 3.4.0 NEWS
NoMethodError is a subclass of NameError, so rescue NameError catches both. NameError and NoMethodError are StandardError`s, but `SystemStackError is not
a StandardError (it descends from Exception), so a bare rescue does not catch it; see
Exceptions and Error Handling. Production code
that matches on message text breaks across versions; match on the class, NameError#name and NameError#receiver instead.
Aliasing, undefining and removing methods
Three operations change which definitions the chain contains, and they differ in a way that matters for lookup.
-
alias new old(keyword) andalias_method :new, :old(method) copy the current definition ofoldunder a second name. The copy is independent: redefiningoldlater does not changenew. The keyword takes bare names and is resolved when the class body runs;alias_methodtakes Symbols or Strings, is a public method since Ruby 2.5, can be called from anywhere (includingclass_evalor a metaprogramming helper) and returns the new name as a Symbol. The keyword can also alias global variables (alias $new $old), whichalias_methodcannot. -
remove_method :namedeletes the method from this class only, so a superclass or included module answers again. It raisesNameErrorwhen this class does not define the method itself. -
undef_method :name(and theundefkeyword) writes a tombstone: the name is blocked for this class and its subclasses even if an ancestor defines it, and a call raisesNoMethodError.Module#undefined_instance_methodslists them, andrespond_to?returnsfalse.
# frozen_string_literal: true
class Greeter
def hello = "hello"
alias howdy hello # the keyword: bare names, resolved when the class body runs
end
Greeter.alias_method(:hi, :hello) # the method: Symbols or Strings, can be called anywhere
# A later redefinition leaves both aliases on the old body.
class Greeter
def hello = "hello, world" # rubocop:disable Lint/DuplicateMethods
end
greeter = Greeter.new
p greeter.hello, greeter.hi, greeter.howdy
p greeter.method(:hi).name, greeter.method(:hi).original_name
p Greeter.instance_method(:hi) == Greeter.instance_method(:howdy)
class Parent
def show = "parent show"
end
class Child < Parent
def show = "child>#{super}"
alias display show
end
p Child.new.display # super still searches for :show
class Base
def greet = "base"
end
class Sub < Base
def greet = "sub"
end
p Sub.send(:alias_method, :old_greet, :greet)
p Sub.send(:remove_method, :greet)
p Sub.new.greet # remove_method lets the superclass answer again
Sub.send(:undef_method, :greet)
begin
Sub.new.greet
rescue NoMethodError => e
puts e.message
end
p Sub.undefined_instance_methods
p Sub.new.respond_to?(:greet), Sub.method_defined?(:greet)
Output:
"hello, world"
"hello"
"hello"
:hi
:hello
true
"child>parent show"
:old_greet
Sub
"base"
undefined method 'greet' for an instance of Sub
[:greet]
false
false
Source: Module#alias_method, Module#remove_method, Module#undef_method and alias and undef
greeter.hello returns the new body while hi and howdy keep the original: aliases are snapshots. method(:hi).original_name is :hello, and the
two aliases compare equal as UnboundMethod`s because they share one definition. In `Child, display is an alias of a method that calls super, and it
still reaches Parent#show, which shows that super follows the name in the original definition. remove_method returns the class and brings
Base#greet back, whereas undef_method makes the call fail even though Base defines it. alias_method on a method that does not exist
raises NameError, as does instance_method.
Module#method_added, method_removed and method_undefined (and the singleton versions) fire for these operations; as always, they must call super.
Wrapping a method
Adding behaviour before or after an existing method is the most common reason to touch the chain. There are three techniques, and only one is recommended in current code.
-
Alias chain (the historical "alias_method_chain"): alias the original to a new name, define the wrapper under the old name, and call the alias. It works on any class and needs no module, but it is not idempotent. If the file that installs the wrapper is loaded twice, the second alias captures the wrapper, the wrapper calls the alias, and the call never ends.
-
Prepend: write a module that defines the method and calls
super, thenprependit. The original stays reachable throughsuper, the module can be reused on other classes,Method#super_methodshows the original, and a secondprependof the same module is a no-op. -
Refinements (
refineandusing) confine the change to a file or scope; see Refinements and Monkey Patching.
The alias chain first. The wrapper below is installed twice, which is what happens when a patch file is required from two places or reloaded in development.
# frozen_string_literal: true
require "shelf"
# The old way: an alias chain, done twice by accident, never terminates.
module Shelf
class Catalog
def self.log_adds
alias_method :add_without_log, :add
define_method(:add) do |book|
add_without_log(book) # now calls this very method
end
end
end
end
isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Dune", author: Shelf::Author.new(name: "Frank Herbert"))
Shelf::Catalog.log_adds
Shelf::Catalog.log_adds # loaded twice: add_without_log now points at the wrapper
begin
Shelf::Catalog.new.add(book)
rescue SystemStackError => e
puts "#{e.class}: #{e.message}"
end
Output:
SystemStackError: stack level too deep
Source: Module#alias_method and SystemStackError
The first call to log_adds aliases the real add to add_without_log and installs the wrapper. The second call aliases the wrapper to add_without_log
and installs a new wrapper, so add_without_log now points to a method that calls add_without_log. The first call of add recurses until the stack is
exhausted. The same program with prepend is stable.
# frozen_string_literal: true
require "shelf"
# The modern way: a prepended module, reusable and removable from the chain only by not prepending it.
module AddLogging
def add(book)
puts "adding #{book.title}"
super
end
end
Shelf::Catalog.prepend(AddLogging)
Shelf::Catalog.prepend(AddLogging) # the second prepend changes nothing
isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Dune", author: Shelf::Author.new(name: "Frank Herbert"))
catalog = Shelf::Catalog.new(store: Shelf::Store::Memory.new)
p catalog.add(book).equal?(catalog)
p catalog.count
p Shelf::Catalog.ancestors.first(3)
p catalog.method(:add).owner
p catalog.method(:add).super_method.owner
p catalog.method(:add).super_method.source_location.last
Output:
adding Dune
true
1
[AddLogging, Shelf::Catalog, Enumerable]
AddLogging
Shelf::Catalog
13
Source: Module#prepend and Method#super_method
The wrapper ran once even though prepend was called twice, ancestors shows AddLogging in front of Shelf::Catalog, and super_method leads to
the original Catalog#add at line 13 of the library. Because super passes the arguments and block, the wrapper does not need to
repeat the signature. Prepending to a class that already has live instances affects them immediately.
The remaining differences between the techniques:
| Aspect | Alias chain | Prepend | Refinement |
|---|---|---|---|
Idempotent |
No, a second installation recurses. |
Yes, a module is added to the chain once. |
Yes. |
Visible to other files |
Everywhere, immediately. |
Everywhere, immediately. |
Only where |
Original reachable as |
The alias name. |
|
|
Introspection |
|
|
|
RuboCop |
|
|
None specific. |
Redefining a method in place (class String; def to_s ...) is the monkey-patching case: it replaces the original with no way to call it, and with -w it emits "method redefined; discarding old to_s". Prefer prepend
when the behaviour must be global, and a refinement when it must be local.
Pitfalls
-
Looking at the class, not the chain.
Shelved.instance_methods(false)lists only what the class itself defines;Shelved.instance_method(:x).ownernames where the answer lives. -
Prepended methods hide the class’s own. A prepended module is searched first, so
Method#ownerfor a method the class defines is the module, and tools that match on the class (source_location) point at the wrapper. -
Bare
superafter changing a parameter sends the changed value. Usesuper()or explicit arguments when that is not what you want. -
superin a module included in two unrelated classes reaches different methods; test the module in the classes it is mixed into. -
Methods defined in a block with
define_methodneed explicitsuper(args)and cannot use baresuper. -
Shared instance variables. Every module in the chain sees the receiver’s instance variables, so two mixins that both use
@cachecollide; prefix the names. -
method_missingwithoutrespond_to_missing?breaksrespond_to?,methodandArray()-style conversions that probe withrespond_to?. -
method_missingthat swallows errors. Not callingsuperfor unknown names converts typos intonil. -
Private calls reach
method_missing. Calling a private or protected method with an explicit receiver is dispatched tomethod_missing, so a ghost-method handler withoutsuperhides "private method called" errors. -
Modifying
ObjectorKernelchanges every lookup in the process. -
Included modules come before
Object. A method the class defines wins; one it does not define comes from the module, and a module method wins over one with the same name inObject,KernelorBasicObject(for exampleComparable#==overBasicObject#==). -
Calling
superfrom a singleton method needs a method of that name in the singleton chain, not in the class chain.
Where older material is outdated
-
Alias chains (
alias_method_chain, a Rails-era helper, and hand-written equivalents) are the idiom of the 1.8 and 1.9 era. Modern code prepends a module. -
Module#includeonly affecting future includers is the pre-3.0 behaviour described above; since Ruby 3.0 later changes propagate. -
method_missingexamples withoutrespond_to_missing?appear in many older tutorials;respond_to_missing?has been the expected companion since Ruby 1.9.2. Current code defines both and ends both withsuper. -
Bare
superin a hook or constructor that passes arguments the parent does not accept is a recurring bug in published examples; check that the parent’s signature accepts what the bare form forwards, or usesuper(). -
Hooks without
supersuch asinheritedandmethod_addedsilence other modules' hooks. -
Error text. Messages with a backtick as the opening quote are from Ruby 3.3 or earlier, and messages that print the receiver as
#<Foo:0x...>are from Ruby 3.2 or earlier. -
Method#unbind/bindeverywhere.bind_call(Ruby 2.7) replaces the two-step form. -
singleton_class.send(:define_method, ...)workarounds are obsolete sincedefine_singleton_methodand the publicdefine_method(Ruby 2.5).
RuboCop cops for lookup and super
The following cops, all available in RuboCop 1.92, encode the rules of this page.
| Cop | Rule it enforces |
|---|---|
|
Constructors and lifecycle hooks ( |
|
A class that defines |
|
A method that only calls |
|
Call |
|
Use |
|
The same method defined twice in one class body. |
|
One |
Source: RuboCop Lint cops and RuboCop Style cops
References
-
Ruby 4.0 documentation: Refinements (method lookup with refinements,
superin a refinement) -
Ruby 4.0 documentation: Miscellaneous syntax (
alias,undef,defined?) -
Module (
ancestors,include,prepend,alias_method,remove_method,undef_method,instance_method), Method and UnboundMethod -
Ruby 3.0.0 NEWS, 3.3.0 and 3.4.0
-
The Ruby Style Guide and RuboCop Lint cops (
Lint/MissingSuper,Style/MissingRespondToMissing)