Modules and mixins

Ruby · Practical · 14 min

Mixins are how Ruby shares behaviour across classes; Enumerable and Comparable are the most-used examples.

A module is a container of methods and constants that can't be instantiated. It has two jobs.

As a namespace, it groups related classes: module Billing; class Invoice; end; end is referenced as Billing::Invoice. Gems put everything under one top-level module to avoid clashes.

As a mixin, it adds behaviour to classes:

  • include Mod adds the module's methods as instance methods. The module is inserted into the class's ancestor chain just above the class.
  • extend Mod adds them as methods on a single object, typically the class itself, so they become class methods.
  • prepend Mod inserts the module below the class, so its methods run first and can call super to reach the class's own method. This is the clean way to wrap existing methods.

Mixins work through contracts. Comparable needs <=> and gives you <, <=, ==, >, between? and clamp. Enumerable needs each and gives you map, select, sort, sum and the rest.

Klass.ancestors shows the lookup order, and obj.is_a?(Mod) is true for included modules. A module's self.included(base) hook runs when it is included, which is how libraries add class methods at the same time (Rails wraps this pattern in ActiveSupport::Concern).

module_function makes utility modules whose methods are called as Mod.method, like Math.sqrt.

Example

class Version
  include Comparable
  attr_reader :parts
  def initialize(s) = @parts = s.split(".").map(&:to_i)
  def <=>(other) = parts <=> other.parts
  def to_s = parts.join(".")
end

p Version.new("1.10.0") > Version.new("1.9.3")    # true
p [Version.new("2.0"), Version.new("1.2")].max.to_s  # "2.0"

class Playlist
  include Enumerable
  def initialize(*songs) = @songs = songs
  def each(&) = @songs.each(&)
end
p Playlist.new("b", "a", "c").sort               # ["a", "b", "c"]
module Loud
  def to_s = super.upcase + "!"
end
class Version
  prepend Loud
end
puts Version.new("3.4")                          # 3.4!
p Version.ancestors.first(3)                     # [Loud, Version, Comparable]

Remember

  • Modules are namespaces and mixins; they can't be instantiated
  • include adds instance methods, extend adds singleton methods, prepend wraps
  • Comparable needs <=>; Enumerable needs each

Watch out for

  • Including a module expecting its methods as class methods; that's extend (or the included hook).

Check yourself

  1. Which keyword lets a module's method run before the class's own method and call it via super?
  2. What must a class define to get Enumerable's methods?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ruby-lang.org/en/3.4/Module.html