Modules and Mixins

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. A module is a container of methods and constants that is not a class: it cannot be instantiated and it cannot be subclassed, but it can be inserted into the ancestor chain of a class (a mixin) or used as a plain namespace. Ruby has single inheritance for classes, and mixins are how code is shared sideways: Comparable gives a class the ordering operators, Enumerable gives it about sixty collection methods, Kernel gives every object puts and format. This page covers modules from both sides, as namespaces and as mixins, then the two protocol modules of the core library, the delegation helpers Forwardable and SimpleDelegator, a taxonomy of what modules are used for, and the caveats (collisions, implicit coupling, testing) that decide when a module is the wrong tool.

It builds on Classes and Objects (a class is a module that can also create instances) and on Methods and Parameters. The lookup rules that decide which of several same-named methods runs are in Method Lookup and super; this page shows the chains and leaves the full algorithm there. Related pages are Singleton Classes and Class-Level Behavior (what extend really changes), Visibility and Encapsulation, Refinements and Monkey Patching (the scoped alternative to reopening a class), Equality, Hashing and Comparison (the full story behind Comparable), Enumerable and Enumerators and Lazy Evaluation (the methods the Enumerable mixin supplies), Hooks, eval and DSLs and Dynamic Dispatch and Method Definition (generating methods and modules at run time), SOLID and Design Patterns (Decorator and Adapter with delegation) and Application Architecture in Plain Ruby. Code loading of the files that define modules is in Code Loading: require and autoload. For how other languages share behaviour, see Rust traits, Swift protocols and Scala traits. Ruby has no interfaces; the closest relatives of a mixin are traits and default methods.

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 off, and Style/OneClassPerFile off because several examples define a few small modules and classes in one file). The output shown is the real output. Where an example breaks a cop on purpose, an inline rubocop:disable comment says so. Most examples are run with ruby -Ilib from the shelf directory, so require "shelf" finds the gem, as described in the shelf gem source.

The model in one page

Module is a class, and Class is a subclass of Module. So a class is a module with extra abilities (new, allocate, superclass), and everything in this page about constants, instance methods and the hooks applies to classes too. A module has two jobs, and most modules do only one of them:

  • Namespace. A module groups constants (classes, other modules, values) under a name so that Shelf::Store::Memory cannot clash with another Memory. It is never included anywhere.

  • Mixin. A module holds instance methods that are inserted into the ancestor chain of a class (include, prepend) or of a single object (extend). The class then responds to those methods as if it had defined them.

The vocabulary used in the rest of the page:

Construct Meaning

module Name ... end

Creates the module, or reopens it when the constant already holds a module. Reopening a constant that holds a class is a TypeError.

Module.new { ... }

An anonymous module; assigned to a constant it gets that constant’s name. Used by builder modules.

include M

Inserts M into the ancestors of the receiver, directly after the receiver itself. M's instance methods become the receiver’s instance methods.

prepend M

Inserts M before the receiver in the ancestors, so M's methods run first and can call super to reach the receiver’s own.

extend M

Inserts M into the singleton class of the receiver, so M's instance methods become singleton methods of the receiver. Defined on Object, not on Module.

module_function

Makes the following methods module-level functions: a public singleton copy and a private instance method.

extend self

A module extends itself so that its own instance methods can be called on the module.

included, extended, prepended

Hooks called after the module has been mixed in. Private, empty by default.

append_features, prepend_features, extend_object

The methods that perform the mixing. included is called by include after append_features.

M === obj, obj.is_a?(M)

True when M is in the ancestors of obj's class or of its singleton class.

ancestors, included_modules, include?

Reflection on the chain, covered in Inspecting modules.

private_constant, const_get, Module.nesting

Constant visibility and lookup, covered in Constants and modules.

The relationships in the running example, drawn as a class diagram. Branch is a library branch; it prepends Audit, includes Searchable and Enumerable, extends Registry and is created through the usual constructor. The full chain is shown a few sections below.

classDiagram direction LR class Module class Class Module <|-- Class class Audit { <<module>> +add(book) } class Searchable { <<module>> +search(text) } class Registry { <<module>> +branches() +open_branch(name) } class Enumerable { <<module>> +map() +select() } class Branch { +name +add(book) +each() } Branch ..> Audit : prepend Branch ..> Searchable : include Branch ..> Enumerable : include Branch ..> Registry : extend

Modules as namespaces

A module definition creates a constant. Constants nest: Shelf::Store::Memory is the constant Memory found inside the module Shelf::Store, which is found inside Shelf. Namespaces are how a gem keeps its names out of the way of every other gem (the convention is one top-level module per gem, named after it) and how a program groups related classes. A module used only as a namespace has no instance methods and is never included.

The next file reopens Shelf::Store with the compact module Shelf::Store form and shows what that costs. The nesting of the body, Module.nesting, contains only Shelf::Store, not Shelf, so a constant that lives in Shelf (such as Config) is not found by the lexical lookup; the nested form module Shelf followed by module Store has both modules in the nesting and finds it. The file also confirms that a module is an instance of Module, a class is an instance of Class, and that Class inherits from Module.

# frozen_string_literal: true

require "shelf"

module Shelf::Store # rubocop:disable Style/ClassAndModuleChildren
  def self.where = Module.nesting
  def self.memory = Memory
  def self.config = Config
end

p Shelf::Store.where
p Shelf::Store.memory
p Shelf::Store.constants
p Shelf::Store.instance_of?(Module)
p Shelf::Store::Memory.instance_of?(Class)
p Class.superclass
begin
  Shelf::Store.config
rescue NameError => e
  puts e.message
end
begin
  Shelf::Store.new
rescue NoMethodError => e
  puts e.message
end

Output:

[Shelf::Store]
Shelf::Store::Memory
[:Memory]
true
true
Module
uninitialized constant Shelf::Store::Config
undefined method 'new' for module Shelf::Store

The rules that matter for namespaces:

  • module Shelf::Store requires that Shelf already exists. It does not create Shelf for you, and it does not make Shelf's constants visible inside (see above). RuboCop’s Style/ClassAndModuleChildren prefers the nested form for exactly that reason.

  • The :: operator reaches into a module (Shelf::Store::Memory); a leading :: (::String) starts the lookup at Object, which is how code inside a module reaches a top-level constant that the module shadows.

  • The lookup of a bare constant checks the lexical nesting from the innermost module outward, then the ancestors of the innermost one, then Object. The full rules, including autoload and const_missing, are in Variables, Constants and Scope.

  • A module name is a constant, so it is CamelCase. Acronyms stay capitalised (HTTPClient) by convention, and the file name is the snake_case form (lib/shelf/store/memory.rb).

  • Calling new on a module is a NoMethodError (undefined method 'new' for module Shelf::Store), as the output shows.

Modules as mixins

A mixin module is written once and inserted into any number of classes. The example below defines three modules and one class, the shelf library branch. Searchable is included, Audit is prepended and Registry is extended; the file prints the two ancestor chains and the owner of one method from each.

# frozen_string_literal: true

require "shelf"

module Shelf
  # Prepended: runs before the class's own methods.
  module Audit
    def add(book)
      puts "audit: #{book.title}"
      super
    end
  end

  # Extended: becomes a class-level capability.
  module Registry
    def branches = (@branches ||= [])
    def open_branch(name) = new(name).tap { |branch| branches << branch }
  end

  # Included: the instance-level capability.
  module Searchable
    def search(text) = select { |book| book.title.downcase.include?(text.downcase) }
  end

  class Branch
    prepend Audit
    include Enumerable
    include Searchable
    extend Registry

    attr_reader :name

    def initialize(name)
      @name = name
      @books = []
    end

    def add(book)
      @books << book
      self
    end

    def each(&) = @books.each(&)
  end
end

p Shelf::Branch.ancestors
p Shelf::Branch.singleton_class.ancestors.first(4)

branch = Shelf::Branch.open_branch("Central")
author = Shelf::Author.new(name: "Ursula K. Le Guin")
isbn = Shelf::Isbn.new(digits: "978-0-441-47812-5")
branch.add(Shelf::Book.new(isbn:, title: "The Dispossessed", author:))
puts branch.search("dispo").map(&:to_s)
p branch.method(:add).owner
p branch.method(:search).owner
p Shelf::Branch.method(:open_branch).owner

Output:

[Shelf::Audit, Shelf::Branch, Shelf::Searchable, Enumerable, Object, Kernel, BasicObject]
[#<Class:Shelf::Branch>, Shelf::Registry, #<Class:Object>, #<Class:BasicObject>]
audit: The Dispossessed
The Dispossessed by Ursula K. Le Guin
Shelf::Audit
Shelf::Searchable
Shelf::Registry

The figure draws those two chains. Ruby searches a chain from the top and runs the first method it finds; a method that calls super continues down the chain.

The two ancestor chains of Shelf::Branch. Instance lookup: the prepended Shelf::Audit, then Shelf::Branch, then the included Shelf::Searchable and Enumerable, then Object, Kernel and BasicObject. Class-level lookup: the singleton class of Branch, the extended Shelf::Registry, the singleton classes of Object and BasicObject, then Class, Module, Object, Kernel and BasicObject
Call Where the module lands Typical use

include M

After the receiver class, before its superclass. The instance methods of M become instance methods.

Adding behaviour that the class may override: Comparable, Enumerable, a Searchable helper.

prepend M

Before the receiver class. M's methods run first; the class’s own method is reached with super.

Wrapping or decorating the class’s own methods: auditing, logging, memoizing, instrumentation.

extend M

In the singleton class of the receiver. The instance methods of M become singleton methods.

Class-level macros and factories (extend Registry), or adding behaviour to one object.

What the output teaches:

  • Shelf::Branch.ancestors lists Shelf::Audit before the class (prepend), then the class, then Shelf::Searchable, then Enumerable (include), then Object, Kernel and BasicObject. A class with a superclass would list the superclass and its mixins after its own.

  • Shelf::Searchable comes before Enumerable although it was included second: the most recently included module is searched first. When two modules define the same method, the later include wins.

  • branch.method(:add).owner is Shelf::Audit, not Shelf::Branch: the prepended module’s add is what runs, and its super reaches the class’s add. The audit: line printed before the books were stored shows the order.

  • extend Registry put the module in Shelf::Branch.singleton_class.ancestors, behind the singleton class. The instance methods of Registry (branches, open_branch) became class methods of Branch, and the ivar @branches lives on the class object, not on the instances.

  • include and prepend take effect on instances created before and after the call alike, because the chain is shared; they are not copies. Ruby implements this by inserting a hidden proxy for the module into the chain (an implementation detail that ancestors hides). Redefining a method inside the module later changes every includer.

The order of includes, duplicates and what include rejects

include accepts several modules at once, include A, B, and inserts them so that A ends up closer to the class, which is the same chain as include B followed by include A. Spelled out in separate statements it is easier to read (RuboCop’s Style/MixinGrouping asks for that). The next file shows the order, the rule that a module already in the chain is skipped (also when it was included by a superclass), the cooperative super chain it produces, and the three errors that mixing can raise.

# frozen_string_literal: true

module Loud
  def speak = "#{super.upcase}!"
end

module Polite
  def speak = "please, #{super}"
end

class Voice
  def speak = "hello"
end

class Greeter < Voice
  include Loud, Polite # rubocop:disable Style/MixinGrouping
  include Loud # already there: ignored
end

p Greeter.ancestors.first(4)

class Child < Greeter
  include Loud # already in the superclass chain: nothing changes
end

p Child.ancestors.first(4)
puts Greeter.new.speak
p Greeter.include?(Polite)
p Greeter.included_modules.first(2)
p Greeter.new.is_a?(Loud)
p Loud === Greeter.new # rubocop:disable Style/CaseEquality

class Late < Voice
  include Loud
  include Polite # the most recent include is searched first
end
p Late.ancestors.first(4)
puts Late.new.speak

module Util; end

[
  -> { Class.new { include String } },
  -> { Util.new }
].each do |attempt|
  attempt.call
rescue StandardError => e
  puts "#{e.class}: #{e.message}"
end

module Outer; end

module Inner
  include Outer
end
begin
  Outer.include(Inner)
rescue ArgumentError => e
  puts "#{e.class}: #{e.message}"
end

Output:

[Greeter, Loud, Polite, Voice]
[Child, Greeter, Loud, Polite]
PLEASE, HELLO!
true
[Loud, Polite]
true
true
[Late, Polite, Loud, Voice]
please, HELLO!
TypeError: wrong argument type Class (expected Module)
NoMethodError: undefined method 'new' for module Util
ArgumentError: cyclic include detected
  • Greeter includes Loud then Polite in one call and a second include Loud is silently ignored, so Loud appears once. Child < Greeter also ignores include Loud: a module already present in the superclass chain is not inserted again. (prepend is different: a module in the superclass chain can still be prepended to the subclass.)

  • In Late, the statement order is reversed and so is the chain. Because both modules call super, the speak method of the nearest module runs first, and the output differs: PLEASE, HELLO! against please, HELLO!.

  • include String raises TypeError (wrong argument type Class (expected Module)): only modules can be mixed in. Inherit from a class with <.

  • Util.new shows again that a module cannot be instantiated.

  • Including a module in a module it already contains raises ArgumentError (cyclic include detected), here Outer.include(Inner) when Inner already includes Outer.

  • is_a? and === are true for the class’s instances and for the module: Loud === Greeter.new is the same test that case uses, so case object when Comparable works.

Including into a module that is already included (Ruby 3.0)

Before Ruby 3.0 a module that was included in a class and then changed with include did not pass the new module on to the class, which surprised everyone who built modules in several steps. Since Ruby 3.0, Module#include and Module#prepend also affect the classes and modules that have already included or prepended the receiver. Ruby 3.1 extended that to prepend when the receiver already includes the argument.

# frozen_string_literal: true

module Base; end

module Extra
  def extra = :extra
end

class Host
  include Base
end

Base.include(Extra) # after Host already included Base

p Host.ancestors.first(3)
p Host.new.respond_to?(:extra)
p Host.include?(Extra)

Output:

[Host, Base, Extra]
true
true

Older code and older books use workarounds for this (reopening every includer). They are unnecessary now.

extend: mixing into one object

extend is available on every object. object.extend(M) is the same as object.singleton_class.include(M): the module goes into the singleton class of that one object, and only that object gains the methods. extend returns the receiver, so it chains. This is also how a class gets class-level methods, since a class is an object.

# frozen_string_literal: true

require "shelf"

module Shelf
  module Greeting
    def greet = "welcome to #{name}"
  end

  class Branch
    attr_reader :name

    def initialize(name) = @name = name
  end
end

central = Shelf::Branch.new("Central")
annex = Shelf::Branch.new("Annex")
result = central.extend(Shelf::Greeting)

p result.equal?(central)
puts central.greet
p annex.respond_to?(:greet)
p central.singleton_class.ancestors[1..2]
p central.singleton_class.include?(Shelf::Greeting)
p central.is_a?(Shelf::Greeting)
p Shelf::Branch.include?(Shelf::Greeting)
p central.singleton_methods
p central.singleton_class.instance_methods(false)

# At the top level, `include` targets Object: every object receives the module.
include Comparable # rubocop:disable Style/MixinUsage

p Object.include?(Comparable)
p 5.is_a?(Comparable)
p inspect

Output:

true
welcome to Central
false
[Shelf::Greeting, Shelf::Branch]
true
true
false
[:greet]
[]
true
true
"main"
  • central gained greet; the other Branch, annex, did not, and Shelf::Branch itself does not include the module. central.is_a?(Shelf::Greeting) is nevertheless true, because is_a? looks at the singleton class.

  • singleton_methods includes the methods that come from extended modules (its all argument defaults to true), while singleton_class.instance_methods(false) lists only those defined directly on the singleton class.

  • At the top level, self is the object main, and include called there is a private method of main that includes the module in Object. Every object of the program, 5 included, then has the module. That is rarely intended; RuboCop’s Style/MixinUsage flags it. Put include inside a class or module.

Choosing between the three

A short decision aid:

You want Use

The class to get a set of methods it may override

include

To wrap the class’s own methods, calling super

prepend

Methods on the class object itself (macros, factories, registries)

extend, usually through included

Methods on one object only (a test double, a configured instance)

object.extend(M)

A pure function container with no state

module_function, or def self.name in the module

A shared name prefix

A namespace module

Reuse of a data structure with state of its own

Composition (a collaborator object), or delegation

Hooks and the ClassMethods idiom

Mixing is performed by three private methods of Module and announced by three hooks. All six are private and are called by Ruby; a module defines them as singleton methods (def self.included(base)) to run code when it is mixed in.

Method When it runs

append_features(base)

Called by include. The default implementation inserts the module into base's chain. Overriding it without calling super prevents the mixing.

included(base)

Called by include after append_features. Empty by default; the usual place to extend base with class methods.

prepend_features(base)

The prepend counterpart of append_features.

prepended(base)

The prepend counterpart of included.

extend_object(object)

Called by extend. The default implementation puts the module into the object’s singleton class.

extended(object)

Called by extend after extend_object.

The next file logs all of them, then shows a module whose append_features does not call super: the included hook still runs, but nothing is mixed in.

# frozen_string_literal: true

module Tracer
  def self.included(base)
    puts "included in #{base}"
    super
  end

  def self.extended(base)
    puts "extended an instance of #{base.class}"
    super
  end

  def self.prepended(base)
    puts "prepended to #{base}"
    super
  end

  def self.append_features(base)
    puts "append_features(#{base})"
    super
  end

  def self.prepend_features(base)
    puts "prepend_features(#{base})"
    super
  end

  def self.extend_object(object)
    puts "extend_object"
    super
  end

  def traced = :traced
end

class A
  include Tracer
end

class B
  prepend Tracer
end

Object.new.extend(Tracer)

module Blocker
  # Never calls super: nothing is mixed in.
  def self.append_features(_base) = nil

  def self.included(base) = puts("included hook still runs for #{base}")
  def blocked = :never
end

class C
  include Blocker
end
p C.include?(Blocker)
p C.new.respond_to?(:blocked)

Output:

append_features(A)
included in A
prepend_features(B)
prepended to B
extend_object
extended an instance of Object
included hook still runs for C
false
false

Two conventions for hooks: call super (the default does nothing today, but another module in the chain may implement the hook, so passing the call along costs nothing) and keep the hook short.

self.included(base) and ClassMethods

A module can only give instances its instance methods. To also give the class some class-level methods, the module carries a nested module, conventionally named ClassMethods, and extends the includer from the included hook. This is the plain-Ruby idiom, and a large share of the Ruby libraries written before 2015 follow it. The example below defines a Trackable module whose track macro declares which attributes make up a snapshot and defines a reader for each; the inherited hook copies the declarations into subclasses (see Hooks, eval and DSLs).

# frozen_string_literal: true

require "shelf"

module Shelf
  # Gives a class a `track` macro and instance readers for the tracked attributes.
  module Trackable
    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      def tracked = (@tracked ||= [])

      def track(*names)
        tracked.concat(names)
        names.each { |name| define_method(:"tracked_#{name}") { public_send(name) } }
      end

      def inherited(subclass)
        super
        subclass.tracked.concat(tracked)
      end
    end

    def snapshot = self.class.tracked.to_h { |name| [name, public_send(name)] }
  end

  class Copy
    include Trackable

    attr_reader :barcode, :condition

    track :barcode, :condition

    def initialize(barcode, condition)
      @barcode = barcode
      @condition = condition
    end
  end

  class DamagedCopy < Copy
    track :repair_note

    def repair_note = "spine torn"
  end
end

copy = Shelf::Copy.new("B-001", :good)
p copy.snapshot
p copy.tracked_barcode
p Shelf::DamagedCopy.new("B-002", :poor).snapshot
p Shelf::Copy.singleton_class.include?(Shelf::Trackable::ClassMethods)
p Shelf::Copy.ancestors.first(2)

Output:

{barcode: "B-001", condition: :good}
"B-001"
{barcode: "B-002", condition: :poor, repair_note: "spine torn"}
true
[Shelf::Copy, Shelf::Trackable]

The shape is always the same: instance-level methods directly in the module, class-level methods in a nested ClassMethods, and base.extend(ClassMethods) in included. Notes:

  • Some older code splits the instance methods into a nested InstanceMethods module too and mixes it in from the hook. That adds nothing: the methods defined directly in the module are already instance methods, and the nested module only makes the ancestor chain longer. Define them in the module.

  • ClassMethods is a convention, not a language rule; the name only has to be consistent. Singleton (from the standard library) is built the same way: its included hook extends the class.

  • The hook runs when the include statement executes, so methods the class defines later are not visible to it. Use the macro form (track :barcode) after the attributes exist, or method_added.

  • To run a block of code in the class when the module is included, use base.class_eval { ... } in the hook. That is the whole mechanism behind the next paragraph.

How ActiveSupport::Concern relates to this

Rails' ActiveSupport::Concern packages the idiom above: a module that extends ActiveSupport::Concern can declare an included do ... end block (run in the context of the includer, like class_eval in the hook), a class_methods do ... end block (a generated ClassMethods) and prepended, and it resolves dependencies between concerns so that a concern that includes another one passes it on to the final class. It is a Rails library, not part of Ruby; plain Ruby needs only the included hook and a nested module, as shown, and a library that must work outside Rails should use them. Rails is out of scope for this reference; see the Rails API documentation linked in the references for the details.

Module functions and static containers

Some modules are just a place to keep functions that need no state: conversion helpers, validators, calculators. Ruby has three ways to write one, and they differ in what the includers see.

  • def self.name defines a singleton method of the module. Nothing is copied or inserted anywhere; the module is the only caller. This is the plainest form.

  • module_function (without arguments, it affects the methods defined after it; with names, those methods) makes a copy of each method as a public singleton method of the module and turns the original instance method private. So the module can be called as Math.sqrt(16) and a class that includes it can call sqrt(16) internally, but not from outside (obj.sqrt(16) is a private method call error). This is how Math and Kernel are built.

  • extend self makes the module its own mixin: the instance methods become callable on the module, and they stay public and shared (no copy) when the module is included.

# frozen_string_literal: true

module Shelf
  # A static-function container: no state, no instances.
  module Titles
    module_function

    def normalize(title) = title.strip.squeeze(" ").downcase

    def slug(title) = normalize(title).tr(" ", "-")
  end

  # The same idea with extend self: the methods stay public when included.
  module Isbns
    extend self # rubocop:disable Style/ModuleFunction

    def compact(text) = text.delete("-")
  end

  # A singleton method: no instance copy at all.
  module Money
    def self.cents(amount) = (amount * 100).round
  end
end

puts Shelf::Titles.slug("  The   Left Hand of Darkness ")
p Shelf::Titles.respond_to?(:slug)
p Shelf::Titles.instance_method(:slug).owner
p Shelf::Titles.private_instance_methods.sort
p Shelf::Isbns.compact("978-0-441-47812-5")
p Shelf::Isbns.public_instance_methods

class Importer
  include Shelf::Titles
  include Shelf::Isbns

  def import(title) = slug(title)
end

puts Importer.new.import("Always Coming Home")
p Importer.new.respond_to?(:slug)
p Importer.new.respond_to?(:compact)
begin
  Importer.new.slug("x")
rescue NoMethodError => e
  puts e.message
end

# module_function copies the method: later redefinitions are not mirrored.
module Shelf
  module Titles
    remove_method :slug

    def slug(title) = "redefined-#{title}" # rubocop:disable Lint/DuplicateMethods
  end
end
puts Shelf::Titles.slug("A")
puts Importer.new.import("A")

p Shelf::Money.cents(12.5)

p Math.private_method_defined?(:sqrt)
p Math.sqrt(16)
p Kernel.respond_to?(:format)
p Kernel.private_method_defined?(:format)

Output:

the-left-hand-of-darkness
true
Shelf::Titles
[:normalize, :slug]
"9780441478125"
[:compact]
always-coming-home
false
true
private method 'slug' called for an instance of Importer
a
redefined-A
1250
true
4.0
true
true
  • Shelf::Titles.slug works because module_function copied the method to the singleton class; Importer can call slug internally because the private instance copy is mixed in, and Importer.new.slug("x") is the error private method 'slug' called for an instance of Importer.

  • Isbns (with extend self) has public instance methods, so Importer.new.compact is callable from outside: the helper leaked into the public interface of the includer.

  • The two copies are independent. After the instance method slug is replaced (remove_method first, to avoid the "method redefined" warning), Shelf::Titles.slug("A") still runs the old copy and Importer#import runs the new one. module_function is a snapshot. Redefining the method with def self.slug is the only way to change the module-level one. RuboCop reports the redefinition as Lint/DuplicateMethods.

  • Since Ruby 3.1, module_function, private, public and protected return their arguments, so private def x and module_function def x work as one-liners: the argument is the symbol that def returns.

  • Math.private_method_defined?(:sqrt) is true and Kernel.private_method_defined?(:format) is true: the core library uses the same pattern, which is why include Math lets a class call sqrt without a receiver.

Use module_function when the module is both a namespace of functions and meant to be included for convenience; use def self.name when it is only a namespace; use extend self rarely, since the leak into the includer’s public interface is the usual surprise. RuboCop’s Style/ModuleFunction prefers module_function over extend self.

Comparable

Comparable is the mixin for classes whose objects have an order. The contract is a single method, <=>, that returns a negative number, zero or a positive number for less, equal and greater, and nil when the receiver and the argument cannot be compared. In exchange Comparable implements <, <=, >, >=, ==, between? and clamp.

The example orders anything that has a title. It is a module that includes Comparable itself, so the including class gets both with one include. <=> returns nil for objects without a title, which is the contract’s way of saying "not comparable".

# frozen_string_literal: true

require "shelf"

module Shelf
  # Orders anything that has a title.
  module ByTitle
    include Comparable

    def <=>(other)
      return nil unless other.respond_to?(:title)

      title.casecmp(other.title)
    end
  end

  class Edition
    include ByTitle

    attr_reader :title, :year

    def initialize(title, year)
      @title = title
      @year = year
    end

    def inspect = "#<Edition #{title} #{year}>"
  end
end

a = Shelf::Edition.new("Dune", 1965)
b = Shelf::Edition.new("dune", 2021)
c = Shelf::Edition.new("Emma", 1815)

p a == b
p a.equal?(b)
p a < c
p c.between?(a, c)
p c.clamp(a, b)
p c.clamp(..b)
p [c, a].sort
p [c, a, b].min
p [a, b, c].uniq.size
p({ a => 1 }.key?(b))
p a == "Dune"
p Shelf::ByTitle.ancestors
p Shelf::ByTitle.instance_methods(false)

Output:

true
false
true
true
#<Edition dune 2021>
#<Edition dune 2021>
[#<Edition Dune 1965>, #<Edition Emma 1815>]
#<Edition Dune 1965>
3
false
false
[Shelf::ByTitle, Comparable]
[:<=>]

What to note:

  • == from Comparable is true when <=> returns 0, so "Dune" and "dune" are equal here although equal? is false. Comparable#== also returns true for the same object without calling <=>.

  • clamp(min, max) returns the receiver when it lies between, otherwise the nearer bound; clamp(..max) and clamp(min..) accept a beginless or endless range (Ruby 2.7). The minimum must not exceed the maximum.

  • sort, min and max use <=> directly, so a Comparable class is sortable with no further code.

  • Comparable does not define hash or eql?. The three editions compare as equal (a == b), yet [a, b, c].uniq.size is 3 and { a => 1 }.key?(b) is false. An object that is == to another but hashes differently breaks Hash, Set and uniq; to use such objects as keys, define eql? and hash as well. See Equality, Hashing and Comparison.

  • The result a == "Dune" is false: <=> returned nil, and since Ruby 2.3 Comparable#== no longer rescues exceptions raised inside <=> (older books show a version that did).

  • The module itself shows the shape: Shelf::ByTitle.ancestors is [Shelf::ByTitle, Comparable], so a class that includes it has both in its chain.

What goes wrong with <=>

When <=> returns nil, the operators and sorting raise ArgumentError. The message names the receiver and the argument. Object#<=> returns 0 when the two objects are the same object or ==, and nil otherwise, so a class that includes Comparable without defining <=> fails the first time it is compared.

# frozen_string_literal: true

require "shelf"

class Weight
  include Comparable

  attr_reader :grams

  def initialize(grams) = @grams = grams

  def <=>(other) = other.is_a?(Weight) ? grams <=> other.grams : nil

  def inspect = "#<Weight #{grams}g>"
end

class Plain
  include Comparable
end

light = Weight.new(100)
heavy = Weight.new(900)

p light == 100
p light.eql?(light.dup)
[
  -> { light < 100 },
  -> { [light, 3].sort },
  -> { light.clamp(heavy, light) },
  -> { Object.new <=> Object.new }, # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
  -> { Plain.new < 1 }
].each do |attempt|
  p attempt.call
rescue StandardError => e
  puts "#{e.class}: #{e.message}"
end

author = Shelf::Author.new(name: "Octavia E. Butler")
begin
  [author, author].sort
rescue ArgumentError => e
  puts e.message
end

Output:

false
false
ArgumentError: comparison of Weight with 100 failed
ArgumentError: comparison of Weight with 3 failed
ArgumentError: min argument must be less than or equal to max argument
nil
ArgumentError: comparison of Plain with 1 failed

Source: Comparable and Object#<=>

  • light == 100 is false, not an error: == treats an incomparable argument as "not equal".

  • light < 100 and [light, 3].sort raise comparison of Weight with 100 failed (and with 3): the comparison operators and sort cannot work without an order.

  • clamp(heavy, light) with a minimum above the maximum raises min argument must be less than or equal to max argument.

  • Object.new <=> Object.new is nil: the default for two different objects.

  • A class that includes Comparable but defines no <=> (Plain) raises comparison of Plain with 1 failed.

  • Shelf’s Book is a Data object: it has equality but no order, so [book, book].sort raises comparison of Shelf::Book with Shelf::Book failed. Sort with sort_by and a key (see the Enumerable section), or give Book a <=>.

Tips for writing <=>: delegate to the <=> of a field (title <=> other.title); compare several fields with arrays ([last, first] <=> [other.last, other.first]); return nil for foreign classes; never subtract the two values, because the result overflows or loses precision with floats. Comparable is for objects with a natural total order; for a value object that only needs equality, Data already provides ==.

Enumerable

Enumerable is the mixin that turns an each method into a whole collection API. The contract is one method: each must yield each element, one at a time. Given that, Enumerable supplies map, select, reject, find, sort_by, group_by, partition, each_slice, each_cons, inject, sum, min, max, tally, zip, lazy, to_a, to_set and many more, all built on each. Array, Hash, Range, Struct, Set and IO use it. The full catalogue and the details of each method are in Enumerable; this section covers the mixin side: what the contract is and what it does and does not give the class.

The shelf Catalog includes Enumerable and defines each, so it inherits the whole API. The file below builds a catalog and calls a selection of the inherited methods.

# frozen_string_literal: true

require "shelf"

catalog = Shelf::Catalog.new
le_guin = Shelf::Author.new(name: "Ursula K. Le Guin")
butler = Shelf::Author.new(name: "Octavia E. Butler")
{
  "978-0-441-47812-5" => ["The Dispossessed", le_guin],
  "978-0-8070-8357-4" => ["Kindred", butler],
  "978-0-06-012513-4" => ["The Left Hand of Darkness", le_guin]
}.each do |digits, (title, author)|
  catalog.add(Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author:))
end

puts catalog.map(&:title).inspect
puts catalog.min_by(&:title)
puts catalog.group_by { it.author.name }.transform_values(&:size)
p catalog.count
p catalog.first.title
p catalog.first(2).map(&:title)
p catalog.include?(catalog.first)
p catalog.min_by { it.title.length }.title
p(catalog.each_slice(2).map { |slice| slice.map(&:title) })
p catalog.each.class
p catalog.lazy.select { it.author == le_guin }.map(&:title).first(1)
p catalog.to_set.size
p catalog.entries.size
p Enumerable.instance_methods.size > 50
begin
  catalog.sort
rescue ArgumentError => e
  puts e.message
end
p catalog.sort_by { it.isbn.digits }.map(&:title)

Output:

["The Dispossessed", "Kindred", "The Left Hand of Darkness"]
Kindred by Octavia E. Butler
{"Ursula K. Le Guin" => 2, "Octavia E. Butler" => 1}
3
"The Dispossessed"
["The Dispossessed", "Kindred"]
true
"Kindred"
[["The Dispossessed", "Kindred"], ["The Left Hand of Darkness"]]
Enumerator
["The Dispossessed"]
3
3
true
comparison of Shelf::Book with Shelf::Book failed
["The Left Hand of Darkness", "The Dispossessed", "Kindred"]
  • None of these methods is defined in Catalog; Catalog.instance_method(:map).owner is Enumerable (see Inspecting modules). They all call Catalog#each, which takes the mutex, copies the store’s books and yields them.

  • sort needs the elements to be comparable. Book is not, so catalog.sort raises comparison of Shelf::Book with Shelf::Book failed; sort_by { it.isbn.digits } sorts by a key instead. min_by, max_by, sort_by and group_by take a block and never compare the elements themselves.

  • catalog.each.class is Enumerator: Catalog#each returns enum_for(:each) when no block is given, which is what makes each_slice(2), each_with_index and lazy possible. Always write return enum_for(:each) unless block_given? (or return to_enum(:each) { size } unless block_given? for a known size) at the top of each.

  • each(&) forwards the block with the anonymous block parameter (Ruby 3.1), so the catalog does not name it.

The each protocol in detail

The file below shows what Enumerable does with a bare each: methods that need all the elements read them all, methods that can stop early (first, find, take, include?) stop as soon as they have the answer, and lazy and external iteration (next) pull one element at a time. The counter yielded makes the difference visible.

# frozen_string_literal: true

# A minimal Enumerable: only #each is required.
class Countdown
  include Enumerable

  attr_reader :yielded

  def initialize(from)
    @from = from
    @yielded = 0
  end

  def each
    return to_enum(:each) { @from } unless block_given?

    @from.downto(1) do |n|
      @yielded += 1
      yield n
    end
    self
  end
end

c = Countdown.new(5)
p c.first(2)
p c.yielded
p c.each.size
p c.sum
p c.sort
p c.min
p c.lazy.map { it * 2 }.first(1)
e = c.each
p e.next
p e.next

class Broken
  include Enumerable
end

begin
  Broken.new.map { it }
rescue NoMethodError => e
  puts e.message
end

Output:

[5, 4]
2
5
15
[1, 2, 3, 4, 5]
1
[10]
5
4
undefined method 'each' for an instance of Broken
  • first(2) stopped after two yields (yielded is 2): Enumerable breaks out of each once it has enough. An each that must release a resource therefore needs ensure.

  • c.each.size is 5 because the enumerator was created with a size block, to_enum(:each) { @from }. Without the block, size is nil. Enumerable itself defines no size or length: count is the iterating version.

  • sum, sort and min read every element; c.lazy.map { it * 2 }.first(1) computes one value.

  • e = c.each followed by e.next is external iteration (internally it runs each in a fiber); see Enumerators and Lazy Evaluation.

  • Broken includes Enumerable without defining each, so the first call fails with undefined method 'each' for an instance of Broken. The contract is not checked when the module is included.

  • An infinite each is fine as long as the consumer stops: first, take, find, lazy and each_slice with a break. sort or to_a on it never returns.

Enumerable methods group by purpose (the Ruby documentation uses the same families):

Family Methods

Querying

all?, any?, none?, one?, count, include?, member?, tally

Fetching

first, take, take_while, drop, drop_while, min, max, minmax, min_by, max_by, find, detect, find_index, sum

Searching and filtering

select, filter, reject, grep, grep_v, partition, find_all, filter_map, uniq, compact

Mapping and folding

map, collect, flat_map, collect_concat, inject, reduce, each_with_object, zip

Grouping and slicing

group_by, chunk, chunk_while, slice_when, slice_before, slice_after, each_slice, each_cons

Sorting

sort, sort_by, min, max, reverse_each

Iterating

each_entry, each_with_index, reverse_each, cycle

Converting

to_a, entries, to_h, to_set, lazy, chain

Pitfalls of the mixin: an each that yields several values at once (yield key, value) is treated differently by different methods (map passes them as separate block arguments, so { |x| ... } receives only the first, while to_a, select and each_entry pack them into one array); Enumerable methods do not mutate the receiver, and each should not either; a collection that is changed during iteration has undefined results; and an each that returns the wrong value (it should return self) breaks method chains such as catalog.each { ... }.size.

Forwardable and explicit delegation

Inheritance and mixins share code by putting it in the receiver’s ancestor chain. Delegation shares it by holding another object and forwarding some messages to it. The standard library has two tools: Forwardable (a mixin that defines forwarding methods one at a time) and Delegator with its subclass SimpleDelegator and the DelegateClass helper (a wrapper object that forwards everything). Both are default gems of Ruby 4.0, loaded with require "forwardable" and require "delegate"; they work under Bundler without a Gemfile entry.

Forwardable is mixed in with extend, because its methods define instance methods on the class:

Method Effect

def_delegator(accessor, method, ali = method)

Defines ali on the class; calling it calls method on accessor. accessor is a method name, an instance variable name (:@books) or a constant path as a string. Also def_instance_delegator.

def_delegators(accessor, *methods)

The same for several methods at once, without renaming. Also def_instance_delegators.

delegate(hash)

delegate [:size, :empty?] => :@books or delegate size: :@books: the Hash form, keyed by methods or arrays of them. Also instance_delegate.

SingleForwardable

The same methods for one object: extend SingleForwardable on a class or module delegates its class-level calls; the names are def_single_delegator, def_single_delegators, single_delegate.

The accessor expression is evaluated at every call, so "Shelf.config" (any Ruby expression as a string) always reaches the current value, which is what makes delegating to a global configuration possible.

# frozen_string_literal: true

require "forwardable"
require "shelf"

module Shelf
  # A reading list that exposes part of an Array, not all of it.
  class ReadingList
    extend Forwardable
    include Enumerable

    def_delegators :@books, :size, :empty?, :<<, :each
    def_delegator :@books, :first, :next_up
    def_delegator "Shelf.config", :loan_days, :loan_period
    delegate %i[last clear] => :@books

    def initialize = @books = []
  end

  # Delegation at the class level.
  class Directory
    extend SingleForwardable

    def_delegator :"Shelf.config", :store
    def_single_delegators :"Shelf.config", :loan_days
  end
end

list = Shelf::ReadingList.new
list << "Kindred" << "Dawn"
p list.size
p list.next_up
p list.last
p list.map(&:upcase)
p list.loan_period
p list.respond_to?(:push)
begin
  list.push("x")
rescue NoMethodError => e
  puts e.message
end
p Shelf::ReadingList.instance_method(:size).owner
p Shelf::Directory.loan_days
p Shelf::Directory.store.class
p Shelf::ReadingList.instance_method(:size).source_location.last.class

Output:

2
"Kindred"
"Dawn"
["KINDRED", "DAWN"]
14
false
undefined method 'push' for an instance of Shelf::ReadingList
Shelf::ReadingList
14
Shelf::Store::Memory
Integer
  • ReadingList exposes size, empty?, <<, each, last and clear of its array, and first under the name next_up. It does not expose push: list.push("x") is a NoMethodError. This is the main benefit of Forwardable over inheriting from Array or wrapping with SimpleDelegator: the public interface is exactly the list of forwarded methods.

  • def_delegator "Shelf.config", :loan_days, :loan_period forwards to a string expression, evaluated on each call.

  • Shelf::ReadingList.instance_method(:size).owner is Shelf::ReadingList: the forwarding method lives in the class itself, not in a module in the chain, and source_location points at forwardable.rb.

  • SingleForwardable gave the Directory class (not its instances) loan_days and store.

  • Arguments, keyword arguments and the block are passed through. The delegation is by name, so a method added to the target later is not forwarded until it is listed.

  • RDoc and YARD cannot see delegated methods: add a comment such as # @!method size or a docs line by hand (the Forwardable documentation says as much).

RuboCop’s Style/Documentation and the YARD tags are discussed in Documentation with RDoc and YARD. Rails' ActiveSupport has a different delegate :size, to: :books (a Module#delegate with a to: keyword, allow_nil: and prefix:); it is not Forwardable’s delegate and works only with ActiveSupport loaded.

SimpleDelegator and DelegateClass

Delegator is the base class of wrappers: an object that holds a target and sends every message it does not understand to it (through method_missing and respond_to_missing?). SimpleDelegator is the ready-made subclass: pass the target to new, read it with __getobj__ and replace it with __setobj__. It is the Decorator pattern in Ruby: a subclass overrides a few methods and calls super to reach the wrapped object’s version.

DelegateClass(Array) returns a class that has the public instance methods of Array defined as real forwarding methods, ready to be subclassed. Subclassing it makes a wrapper whose interface is known when the class is created.

# frozen_string_literal: true

require "delegate"
require "shelf"

module Shelf
  # A decorator: adds presentation, forwards everything else to the book.
  class FeaturedBook < SimpleDelegator
    def to_s = "* #{super} *"

    def featured? = true
  end

  # A class generated from an existing one, with its methods listed up front.
  class Titles < DelegateClass(Array)
    def initialize = super([])

    def add(title) = tap { push(title.strip) }
  end
end

isbn = Shelf::Isbn.new(digits: "978-0-441-47812-5")
book = Shelf::Book.new(isbn:, title: "The Dispossessed", author: Shelf::Author.new(name: "Ursula K. Le Guin"))
featured = Shelf::FeaturedBook.new(book)

puts featured
puts featured.title
p featured.featured?
p featured.__getobj__.equal?(book)
p featured == book
p book == featured
p featured.eql?(book)
p featured.is_a?(Shelf::Book)
p featured.instance_of?(Shelf::FeaturedBook)
p featured.class
p featured.respond_to?(:author)
p featured.frozen?

featured.__setobj__(book.with(title: "Always Coming Home"))
puts featured.title

titles = Shelf::Titles.new.add(" Kindred ").add("Dawn")
p titles.size
p titles.__getobj__
p titles.is_a?(Array)

begin
  featured.missing
rescue NoMethodError => e
  puts e.message
end

Output:

* The Dispossessed by Ursula K. Le Guin *
The Dispossessed
true
true
true
false
true
false
true
Shelf::FeaturedBook
true
false
Always Coming Home
2
["Kindred", "Dawn"]
false
undefined method 'missing' for an instance of Shelf::FeaturedBook
  • featured.to_s calls the wrapper’s to_s, whose super reaches the book’s: * The Dispossessed by Ursula K. Le Guin *. featured.title is forwarded. featured.featured? exists only on the wrapper.

  • __getobj__ returns the original object (it is the same object, not a copy); __setobj__ swaps the target. The wrapper’s methods do not change with the target, so swap only for objects of the same type.

  • The wrapper is not the book. featured == book is true because Delegator#== compares the target, but book == featured is false (the Data equality asks whether the other object is a Shelf::Book), featured.is_a?(Shelf::Book) is false and Shelf::Book === featured is false, so case featured when Shelf::Book does not match and pattern matching on the class fails. featured.class is the wrapper’s own class. A decorator that must pass such checks needs DelegateClass(Shelf::Book) or the explicit forwarding of is_a?.

  • A Data object cannot be changed, so "changing" the target means swapping it for book.with(title: ...), as the example does.

  • Titles < DelegateClass(Array) accepts the Array’s methods (size, push) and wraps its own add; titles.is_a?(Array) is false for the same reason.

  • A method that the target does not have fails with the wrapper’s name: undefined method 'missing' for an instance of Shelf::FeaturedBook. Private methods of the target are not forwarded (priv and protected ones raise NoMethodError).

  • Delegator also forwards to_s, inspect, hash, ===, <=> and =~ to the target (featured.hash == book.hash and featured.inspect == book.inspect are both true), while class, is_a? and respond_to? stay the wrapper’s own.

Which one to choose:

Tool When

Forwardable

A few explicit methods of a collaborator: the class stays a small, explicit facade.

SimpleDelegator

A decorator that adds or overrides a few methods and forwards everything else; the target can change at run time.

DelegateClass(Klass)

A wrapper whose interface is fixed to Klass, with real methods instead of method_missing.

Inheriting from the core class

Almost never: a subclass of Array or Hash returns plain Array/Hash from many methods, so it is lost on the first map.

A taxonomy of modules

Modules are used for four different jobs, and mixing the jobs in one module is the root of most design problems with them. This classification follows the one that Rails-era architecture writing uses for modules (and that fits plain Ruby as well):

Kind What it is Shelf example How it is used

Behaviour (mixin)

Instance methods that depend on the host’s interface (each, <=>, a few readers)

Comparable, Enumerable, Shelf::Searchable, Shelf::ByTitle

include or prepend

Builder

A module generated at run time from a parameter

Shelf::Lookup.by(:title)

include Lookup.by(:title)

Static-function container

Stateless functions, no instances

Shelf::Titles, Math, Kernel#format

Module.function, or include with module_function

Namespace

Constants only, never mixed in

Shelf, Shelf::Store, Shelf::Refinements

Shelf::Store::Memory

Each kind has its own rules:

  • Behaviour modules are the ones with collisions and implicit coupling (below). They should state what they require from the host, say in a comment or with a small required_methods-style check, and be tested with a minimal host.

  • Builder modules return a new module (Module.new { ... }) configured by the arguments. They are the plain-Ruby way to parameterise a mixin, and appear in libraries as include Something.new(options) or include Something[options].

  • Static-function containers do not need include at all; if they are included, make the helpers private (module_function).

  • Namespaces hold no methods. Do not add helper methods to a namespace "because it is there": the module becomes a behaviour module by accident.

Builder modules

The next file builds a module per attribute. Each call to Lookup.by makes a new module with a find_by_<attribute> method defined by define_method, and an inspect that names it. The anonymous module has no name until it is assigned to a constant; set_temporary_name (Ruby 3.3) gives it a descriptive one for backtraces without making it reachable by a constant.

# frozen_string_literal: true

require "shelf"

module Shelf
  # A builder module: `Lookup.by(:title)` returns a new module.
  module Lookup
    def self.by(attribute)
      Module.new do
        define_method(:"find_by_#{attribute}") do |value|
          find { |item| item.public_send(attribute) == value }
        end

        define_singleton_method(:inspect) { "Shelf::Lookup.by(#{attribute.inspect})" }
      end
    end
  end

  class Index
    include Enumerable
    include Lookup.by(:title)

    def initialize(books) = @books = books

    def each(&) = @books.each(&)
  end
end

isbn = Shelf::Isbn.new(digits: "978-0-441-47812-5")
book = Shelf::Book.new(isbn:, title: "The Dispossessed", author: Shelf::Author.new(name: "Ursula K. Le Guin"))
index = Shelf::Index.new([book])

puts index.find_by_title("The Dispossessed")
p Shelf::Index.ancestors[1]
p Shelf::Index.ancestors[1].name
anonymous = Module.new
p anonymous.name
anonymous.set_temporary_name("fake<audit>")
p anonymous.name
p anonymous.to_s
begin
  anonymous.set_temporary_name("Shelf::Audit")
rescue ArgumentError => e
  puts e.message
end

Output:

The Dispossessed by Ursula K. Le Guin
Shelf::Lookup.by(:title)
nil
nil
"fake<audit>"
"fake<audit>"
the temporary name must not be a constant path to avoid confusion
  • Shelf::Index.ancestors[1] is the generated module; its name is nil (#<Module:0x...> in the default inspect, which the example overrides). A module that will appear in many backtraces deserves a temporary name.

  • set_temporary_name("fake<audit>") is accepted because it is not a valid constant path, so it cannot be confused with a real constant; set_temporary_name("Shelf::Audit") raises ArgumentError (the temporary name must not be a constant path to avoid confusion). Once a module is assigned to a constant it has a permanent name and cannot be renamed.

  • The include in Index takes an expression, not a constant: include Lookup.by(:title) is evaluated first. Every call creates a distinct module, so Index.ancestors contains an anonymous entry; build the module once and keep it in a constant when many classes share it.

  • More on building modules and classes at run time is in Dynamic Dispatch and Method Definition.

Collisions: the last include wins

Two modules that define the same method collide. Ruby does not warn: the later include is searched first and silently shadows the earlier one; a method defined in the class itself shadows both (because the class comes before its included modules in the chain); a prepended module shadows the class. The next file shows each case and the cooperative alternative, in which every module calls super, so the collisions turn into a chain.

# frozen_string_literal: true

module Describable
  def describe = "describable"
end

module Printable
  def describe = "printable"
end

class Shelved
  include Describable
  include Printable
end

class Own
  include Describable

  def describe = "own"
end

class Wrapped
  prepend Describable

  def describe = "wrapped"
end

puts Shelved.new.describe
puts Own.new.describe
puts Wrapped.new.describe
p Shelved.instance_method(:describe).owner
p Shelved.ancestors.first(3)

# Cooperative modules call super, so collisions become a chain.
module Stamped
  def describe = "#{super} (stamped)"
end

module Numbered
  def describe = "#{super} (numbered)"
end

class Item
  def describe = "item"
end

class Stocked < Item
  include Stamped
  include Numbered
end
puts Stocked.new.describe

# A module that relies on its host: the contract is implicit.
module Labelled
  def label = "#{name.upcase}!"
end

class Named
  include Labelled
end

begin
  Named.new.label
rescue NameError => e
  puts e.message
end

Output:

printable
own
describable
Printable
[Shelved, Printable, Describable]
item (stamped) (numbered)
undefined local variable or method 'name' for an instance of Named
  • Shelved includes Describable then Printable, both defining describe: printable wins, and Shelved.instance_method(:describe).owner is Printable. Nothing warned.

  • Own defines describe itself, so the class’s version beats the module’s: a module cannot override the methods its host defines unless it is prepended. That is the main reason to prepend instead of include.

  • Wrapped prepends Describable, which beats the class’s describe. Because that describe does not call super, the class’s version is unreachable.

  • Stocked shows the cooperative style: Stamped and Numbered each call super, so the output is the class’s value wrapped by both. A module that is meant to be combined with others should call super in every method that others may override, and document the order.

  • Labelled#label calls name, which Named does not have. The error is raised only when label runs, and reads undefined local variable or method 'name' for an instance of Named: the module’s requirement on its host is implicit. RuboCop has no cop for this; a comment or a guard in included is the defence.

  • A module that defines instance variables (@cached_price) shares the namespace of instance variables with the host. See the next section.

Implicit coupling and privacy

A mixin’s private methods and instance variables live in the same namespace as the host’s, so a module is coupled to every class that includes it. Two classic failures, in one file: a private helper in the module that happens to share a name with a private method of the host (the host’s wins, and silently changes the module’s result), and a memoised instance variable that the host also uses.

# frozen_string_literal: true

module Pricing
  def price = base * rate

  def cached_price = @cached_price ||= price

  private

  def rate = 1.2
end

class Plain
  include Pricing

  def base = 10
end

class Surcharged
  include Pricing

  def base = 10

  private

  # An unrelated private helper that happens to share the module's private name.
  def rate = 2
end

class Memoized
  include Pricing

  # The same instance variable name as the module's cache.
  def initialize = @cached_price = :unrelated

  def base = 10
end

p Plain.new.price
p Surcharged.new.price
p Memoized.new.cached_price
begin
  Plain.new.public_send(:rate) # rubocop:disable Style/SendWithLiteralMethodName
rescue NoMethodError => e
  puts e.message
end
p Plain.new.send(:rate)
p Pricing.private_instance_methods

Output:

12.0
20
:unrelated
private method 'rate' called for an instance of Plain
1.2
[:rate]
  • Plain.new.price is 12.0 (the module’s rate of 1.2); Surcharged.new.price is 20, because the host’s own private rate shadows the module’s helper. The module’s author did not intend that and the host’s author did not know.

  • Memoized.new.cached_price returns :unrelated: the module’s @cached_price ||= found a value already stored by the host. Module code that keeps state should use a distinctive instance variable name (@shelf_pricing_cached_price), or avoid state, or hold it in a collaborator object.

  • A private method of a module is private in the including class too: public_send(:rate) is private method 'rate' called for an instance of Plain, while send(:rate) bypasses the check. Visibility is covered in Visibility and Encapsulation.

  • Prepending a module gives it the first word, so a prepended module can shadow the class’s private helper too.

Guidelines that follow from this: keep a mixin’s public surface small; give its private helpers module-specific names or put them in a private nested class; document the methods the host must provide; and prefer a collaborator object when the behaviour needs its own state.

Testing a mixin

A behaviour module is tested the way the language suggests: include it into a minimal host class defined in the test, which supplies only the methods the module requires. That keeps the test independent of any real class, documents the contract, and lets one module have many small hosts. Class.new { include M } creates the host inline; an Array or a Struct is often enough for Enumerable-based modules.

# frozen_string_literal: true

require "minitest/autorun"
require "shelf"

module Shelf
  # Requires the host to define #each (it is an Enumerable-like contract).
  module Totals
    def total_pages = sum(&:pages)
  end
end

class TotalsTest < Minitest::Test
  Page = Struct.new(:pages)

  def host_with(*pages)
    Class.new do
      include Enumerable
      include Shelf::Totals

      define_method(:each) { |&block| pages.each(&block) }
    end.new
  end

  def test_total_pages
    assert_equal 30, host_with(Page.new(10), Page.new(20)).total_pages
  end

  def test_mixes_into_enumerables_only
    refute_respond_to Object.new, :total_pages
    assert_includes Shelf::Totals.instance_methods, :total_pages
  end
end

Running the file (ruby -Ilib e15_testing.rb) prints Minitest’s report; the line that matters is the summary:

2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
  • host_with builds an anonymous class that includes Enumerable and the module under test and defines each with define_method over the given pages. The module is tested through its public contract: total_pages sums whatever each yields.

  • The second test checks the negative case with refute_respond_to: an ordinary Object does not respond to total_pages, so the module did not leak into Object.

  • A module that has class methods through ClassMethods is tested by including it in a host and calling the macro, as in the Trackable example above. A module with hooks is tested by including it and checking ancestors, singleton_class.include? and the resulting methods.

  • For design-time doubts about a module that cannot be tested without five collaborators, the tests are telling you the module is doing too much.

The framework itself is in Testing with Minitest and RSpec and Test Doubles (RSpec calls this "shared examples").

Constants and modules

Constants defined in a module belong to the module. A method defined in a module looks up bare constants lexically, where the method was written, not where it ends up being called, and then in the ancestors of the module. So a constant that an including class redefines does not change what the module’s methods see. The example shows that, and the visibility control private_constant.

# frozen_string_literal: true

module Shelf
  module Limits
    MAX_LOANS = 5
    SECRET = "internal"
    private_constant :SECRET

    def limit = MAX_LOANS
    def secret_len = SECRET.length
  end

  class Member
    include Limits

    MAX_LOANS = 99 # does not affect Limits#limit

    def own_limit = MAX_LOANS
  end
end

m = Shelf::Member.new
p m.limit
p m.own_limit
p m.secret_len
p Shelf::Member::MAX_LOANS
p Shelf::Member.const_get(:SECRET)
p Shelf::Limits.constants
begin
  Shelf::Limits::SECRET
rescue NameError => e
  puts e.message
end
p Shelf::Member.constants.sort
p Shelf::Member.constants(false)
p Shelf::Limits.const_source_location(:MAX_LOANS).last

Output:

5
99
8
99
"internal"
[:MAX_LOANS]
private constant Shelf::Limits::SECRET referenced
[:MAX_LOANS]
[:MAX_LOANS]
5
  • Limits#limit returns 5 even for Member, which defines its own MAX_LOANS = 99: the module’s method sees the constant of the module’s lexical scope. Member#own_limit sees 99. If the module should use the host’s constant, the module must call self.class::MAX_LOANS or self.class.const_get(:MAX_LOANS).

  • Member::MAX_LOANS is 99: the class’s own constant shadows the module’s. Member.constants and Member.constants(false) both print [:MAX_LOANS] (the module’s constant of the same name is shadowed, so the name appears once), and Limits.constants does not list the private SECRET.

  • private_constant :SECRET makes Shelf::Limits::SECRET raise NameError (private constant Shelf::Limits::SECRET referenced), yet module methods can use it lexically, Limits#secret_len is 8. const_get(:SECRET) read it: like send, reflection bypasses the privacy. Treat private_constant as documentation of intent, not as security.

  • const_source_location(:MAX_LOANS) reports the file and line of the definition (Ruby 2.7); deprecate_constant marks a constant so that use warns; Module#const_added (Ruby 3.2) is a hook called when a constant is defined.

Inspecting modules

Reflection answers most "where does this method come from?" questions. The file below applies the reflection methods to the Catalog and its mixins.

# frozen_string_literal: true

require "shelf"

p Shelf::Catalog.include?(Enumerable)
p Shelf::Catalog.included_modules.first(2)
p(Shelf::Catalog.ancestors.take_while { it != Object })
p Shelf::Catalog.instance_methods(false).sort
p Enumerable.method_defined?(:map)
p Shelf::Catalog.instance_method(:map).owner
p Shelf::Catalog.new.method(:each).owner
p Comparable.instance_methods.sort
p Enumerable === Shelf::Catalog.new # rubocop:disable Style/CaseEquality
p Shelf::Catalog <= Enumerable
p Enumerable <= Shelf::Catalog
p Shelf::Catalog <=> Enumerable
p Shelf::Catalog <=> String
p Shelf::Store.class
p Shelf::Store.name
p(Shelf::Store.module_eval { self })
p Module.instance_method(:include).owner
p Shelf.singleton_class.instance_methods(false).sort
p Kernel.instance_method(:puts).owner
p Object.include?(Kernel)
p Comparable.instance_of?(Module)

Output:

true
[Enumerable, Kernel]
[Shelf::Catalog, Enumerable]
[:add, :each]
true
Enumerable
Shelf::Catalog
[:<, :<=, :==, :>, :>=, :between?, :clamp]
true
true
false
-1
nil
Module
"Shelf::Store"
Shelf::Store
Module
[:config, :configure]
Kernel
true
true
Call Answer

Klass.ancestors

The chain, in search order: prepended modules, the class, included modules, then the superclass’s chain.

Klass.include?(M), Klass.included_modules

Whether M is mixed in (directly or through a superclass), and the list of modules (Kernel included).

Klass.instance_methods(false)

Only the methods defined in Klass itself ([:add, :each]), without those of the mixins.

Klass.instance_method(:map).owner, obj.method(:map).owner

The module or class that defines the method that would run.

Enumerable === obj, obj.is_a?(Enumerable)

Whether the object’s class (or singleton class) has the module in its chain.

Klass <= M, Klass < M, Klass <=> M

true when M is in Klass's ancestors, false when M is a descendant, nil when unrelated. <=> gives -1, 0, 1 or nil.

M.instance_methods

The public and protected instance methods of the module.

M.name, M.to_s

The constant path, or nil/#<Module:0x...> for an anonymous module.

Module.nesting, Module.constants

The lexical nesting at the call site and the constants visible there.

singleton_class.include?(M)

Whether an extend took place.

The output of the example confirms: Catalog.instance_methods(false) is [:add, :each]; map's owner is Enumerable; each's owner is Catalog; Catalog <= Enumerable is true and Enumerable <= Catalog is false; and Catalog <=> String is nil because they are unrelated. The module-level Comparable.instance_methods.sort shows the seven methods the mixin provides. In the shell, irb and ri Module#include give the same answers (see IRB and Debugging), and the reflection tools in general are in Reflection, Introspection and Tracing.

Modules, inheritance and composition

A mixin is inheritance without a hierarchy: it adds methods to the same object and shares that object’s state. A collaborator is composition: a separate object with its own state, reached through a method call. Choosing between them is a design decision:

Question Mixin (include) Collaborator or delegation

What is the relationship?

"acts as": a Catalog acts as an Enumerable

"has a": a ReadingList has a list of books

Where does the state live?

In the host’s instance variables, shared and implicit

In the collaborator, behind its public methods

Can it be replaced at run time?

No, the chain is fixed (apart from extend on one object)

Yes, pass another object

How is it tested?

With a minimal host

In isolation

Typical cost

Name collisions, implicit requirements on the host

Forwarding methods, one more object

Guidance that comes from the books on clean Ruby design, adapted to Ruby 4.0:

  • Prefer composition to inheritance, and inheritance to a large mixin. Deep inheritance trees make the lookup chain hard to read; a class with more than two or three levels is a candidate for splitting.

  • Do not "extract a mixin" to shrink a God class. Moving 30 methods into Searchable, Reportable and Exportable leaves one object with the same state, the same 100 public methods and a longer ancestor chain. Extract a collaborator (Shelf::Report.new(catalog)) that takes what it needs as arguments.

  • Use a mixin for a protocol that the core library already defines. Comparable, Enumerable and each are the best-understood mixins in Ruby and are cheap to implement.

  • Prefer the smallest public surface. A mixin that adds twenty public methods to a class changes its public interface twenty times.

  • Duck typing needs no module. A class that responds to each and << is a valid store for Shelf::Catalog; the Store::Memory adapter does not include any interface module. Reach for a module when you want to share the implementation, not to declare an interface (Ruby has none; RBS interfaces and respond_to? checks serve that purpose, see Type Checking with RBS, TypeProf, Steep and Sorbet).

  • Global state belongs in an object, not in a module. A module with @config and def self.configure is a singleton with class-level state, shared by every thread; see Singleton Classes and Class-Level Behavior and Threads and the GVL (Shelf.config above is an example of what to wrap in a mutex or make per-thread).

  • A value object that needs an order mixes in Comparable, a collection mixes in Enumerable, and neither needs anything else. Equality of Data objects is already defined; Comparable adds order only (see Struct, Data and Value Objects).

When a behaviour has to be added to a class you do not own, a module is also the vehicle of the safer alternative to reopening it: a refinement (refine String do ... end, activated with using), as in Shelf::Refinements::TitleCase. See Refinements and Monkey Patching.

Where older books and tutorials are outdated

Ruby books and articles from the 1.9 to 3.x period are still good on the concepts of modules, but several statements and idioms need adjusting for Ruby 4.0:

Older statement Current behaviour

InstanceMethods and ClassMethods nested modules, with base.send(:include, InstanceMethods) and base.extend(ClassMethods) in self.included, as the standard shape of a mixin

include and prepend have been public methods since Ruby 2.1, so send is unnecessary. The InstanceMethods module is redundant, because the methods of the module itself are the instance methods; only ClassMethods plus extend in the hook is needed (or a library helper such as ActiveSupport::Concern in Rails).

alias_method_chain (or hand-written alias_method :old, :new pairs) for wrapping a method from a module

Module#prepend (Ruby 2.0) wraps with super and no aliases; the alias approach loops forever when two wrappers stack. Rails deprecated alias_method_chain for this reason. Ruby 4.1 (preview) also adds a deprecation warning for an alias whose original is defined in a prepended module.

Including a module into a module that a class already includes does not reach the class

Since Ruby 3.0 include and prepend propagate to earlier includers; Ruby 3.1 extended it to prepend.

Comparable#== rescues exceptions raised by <=> and returns false

Removed in Ruby 2.3: an exception in <=> now propagates. Return nil from <=> for incomparable operands instead of raising.

Error messages such as undefined method `x' for nil:NilClass

Ruby 3.3 changed the receiver text and 3.4 the quotes: undefined method 'x' for nil, and for objects for an instance of Importer, as all outputs on this page show.

Comparable and Enumerable as the only protocol modules; Struct.new or Class.new(Struct.new(...)) subclasses for value objects

Data.define (Ruby 3.2) is the idiomatic immutable value object, with equality and hash built in; Comparable still adds order. Assign the Data.define result to a constant instead of subclassing it.

Set as a library that needs require "set", and SortedSet

Set is autoloaded since 3.2 and a core class in 4.0; Enumerable#to_set needs no require. SortedSet was removed.

require "forwardable", delegate and singleton described as pure standard library

They remain default gems (loadable without a Gemfile entry); other libraries once in the stdlib are now bundled gems that Bundler needs in the Gemfile (see Standard Library Tour).

Rails' delegate :a, to: :b introduced as Ruby’s delegation

It is ActiveSupport. Plain Ruby has Forwardable#delegate (a Hash form) and def_delegators.

Singleton as @instance ||= new

Not thread-safe; use the Singleton module or a constant created at load time. See Singleton Classes and Class-Level Behavior.

Refinements as an experimental feature of 2.0

Refinements are stable (since 2.1); Refinement#include and prepend are deprecated since 3.1 in favour of import_methods.

Common exceptions and warnings

Message Cause Fix

TypeError: wrong argument type Class (expected Module)

include, prepend or extend called with a class.

Inherit with <, or extract a module.

TypeError: Foo is not a module (or is not a class)

module Foo or class Foo where the constant already holds the other kind.

Rename one of them.

ArgumentError: cyclic include detected

A module includes a module that includes it.

Break the cycle.

NoMethodError: undefined method 'new' for module Util

Instantiating a module.

Use a class, or call the module-level methods.

NameError: uninitialized constant Shelf::Store::Config

A constant looked up from a compact module A::B body, where only A::B is in the nesting.

Use nested module blocks, or a full path.

NameError: private constant ... referenced

A private_constant used with ::.

Use it lexically inside the module, or make it public.

NoMethodError: private method 'slug' called for an instance of Importer

A module_function method used on an instance.

Call Shelf::Titles.slug, or define the method publicly.

NoMethodError: undefined method 'each' for an instance of Broken

Enumerable included without each.

Define each.

ArgumentError: comparison of Weight with 100 failed

<=> returned nil during <, sort, min, max.

Implement <=> and compare like with like.

ArgumentError: min argument must be less than or equal to max argument

clamp with an inverted range.

Swap the bounds.

ArgumentError: the temporary name must not be a constant path to avoid confusion

set_temporary_name with a name like Shelf::Audit.

Use a name that is not a valid constant path.

warning: method redefined; discarding old slug (with -w)

A module or class redefines its own method.

Remove one of the definitions, or remove_method first.

The warning is printed only in verbose mode; RuboCop catches it statically as Lint/DuplicateMethods. Other cops that police modules: Style/MixinUsage (top-level include), Style/MixinGrouping, Style/ModuleFunction, Style/ClassAndModuleChildren, Lint/ConstantReassignment, Layout/EmptyLinesAroundModuleBody, Style/Documentation, and Style/AccessModifierDeclarations for private in modules.

Pitfalls

  • extend self leaks the helpers into the includer’s public interface. Use module_function for helpers.

  • module_function copies. Redefining the instance method later does not change Module.method; and calling module_function without arguments affects every later def in the body, including ones you wanted public.

  • Including a module after the class defines the method does not override it. The class wins; prepend is the way to override.

  • The last include wins on collisions, silently. Check ancestors and instance_method(:name).owner when the wrong method runs.

  • include at the top level pollutes Object. Every object, including integers, gets the methods.

  • Instance variables and private methods of a module collide with the host’s. Choose distinctive names or avoid state.

  • Comparable without hash and eql?. Equal objects may not be treated as equal by Hash, Set and uniq.

  • <=> returning nil or raising. nil means "not comparable"; the operators then raise ArgumentError. Never subtract values.

  • Enumerable with a each that forgets enum_for. each_slice(2) or each_with_index without a block then fails, and catalog.each is not chainable.

  • Enumerable#sort on non-comparable elements. Use sort_by with a key.

  • SimpleDelegator is not the wrapped class. is_a?, ===, case and pattern matching do not see the target’s class; book == featured and featured == book can differ.

  • Forwardable forwards only what is listed. A new method of the target is not exposed until you add it, and documentation tools do not see forwarded methods.

  • private_constant is advisory. const_get reads it.

  • Hooks must call super. A hook that forgets super hides the same hook from other modules and from Class#inherited-style chains; an append_features that forgets it prevents the mixing entirely.

  • Overusing mixins. A class that includes ten modules is a God class in disguise: the chain is the only place to look.

  • A module is a singleton too. @state on a module is global state shared by all threads.

Version notes

Version Change

2.0

Module#prepend and refinements (experimental) added.

2.1

Module#include and Module#prepend became public; refinements stabilised.

2.3

Comparable#== no longer rescues exceptions raised by <=>.

2.7

Comparable#clamp accepts a Range; Module#const_source_location.

3.0

Module#include and Module#prepend affect the classes and modules that already included or prepended the receiver; public, private, public_class_method and private_class_method accept a single array of names.

3.1

Module#prepend modifies the chain when the receiver already includes the argument; private, public, protected and module_function return their arguments; Refinement#include and #prepend deprecated, import_methods added; anonymous block forwarding &.

3.2

Module#const_added, Module#undefined_instance_methods, Module#refinements; Set autoloaded; Data.

3.3

Module#set_temporary_name.

3.4

it block parameter; undefined method 'x' for an instance of Foo message format with straight quotes.

4.0

Set is a core class (Enumerable#to_set needs no require). Nothing in the mixin mechanics changed.

4.1 (preview)

Not released as of 2026-10-18. The development NEWS lists Module#descendants, and a deprecation warning for alias and alias_method when the original method is defined in a prepended module.

References