Ruby from the ground up

Modules: shared behaviour, and names for things

A module cannot be built, and that is the point: it holds methods to be mixed into classes, and it gives namespaces so two Items can live in one program.

Part 11 of 13

Advanced18 min readBack to Courses

Behaviour that belongs to more than one class

A module is a container for methods, declared like a class but never built with new. Its first job is sharing: methods written inside the module can be mixed into any class with include, and from that moment every object of the class answers them. Where inheritance says is a, a mixin says can do this — and a class may include as many modules as it likes, which is how Ruby offers the good part of multiple inheritance without the ambiguity.

include adds the methods as instance methods. extend adds them to one object on its own, and a module used with extend inside a class adds the methods to the class itself, which is how many libraries define helpers like attr_reader — modules all the way down.

Enumerable: define one method, receive a dozen

The standard library is full of mixins, and the most instructive is Enumerable. A class that includes it and defines just one method — each, yielding its items in turn — suddenly has map, select, sort_by, min_by, group_by, include?, sum and dozens more. All of them are written in terms of each, and none of them are written by you.

That is worth pausing on, because it is the clearest statement of Ruby's design: the language gives you small pieces that compose, and a class of twenty lines can behave like a collection. A Playlist that knows how to go one song at a time knows how to be sorted, searched and summarised without any of those words appearing in its source.

Namespaces, and constants next door

The second job of a module is a surname. module Shop; class Item; end; end puts a class inside a namespace, reached from outside as Shop::Item. Two libraries may both define an Item as long as one is Shop::Item and the other Warehouse::Item — and as programs grow, this is how a collection of related classes stays greppable and its names stay short.

Constants declared in a module belong to it and are reached the same way, Shop::TAX_RATE, which is a tidy place for the numbers that belong to a corner of the program. Nesting modules is ordinary — Shop::Payments::Gateway — and each colon pair is one level deeper.

The ancestor chain

When you include a module, Ruby inserts it into the class's ancestor chain, between the class and its superclass. Method lookup walks that chain, which means a method defined in the class beats one from the module, and the module beats the superclass. prepend puts the module in front of the class instead, which is how a wrapper can run before the original.

extend is the same machinery pointed at a single object: it adds the module's methods to that object's own singleton class. A module used to group class-level helpers is usually extended, not included, because the methods are meant to be called on the class. Namespacing is separate again — a module used as a container is never mixed in at all.

Include is a lookup rule

include does not copy methods into the class. It inserts the module into the ancestor chain, and method lookup walks that chain when a message arrives. That is why a method defined in the class overrides the module’s version, and why a later include can override an earlier one.

The chain also explains super: it continues the search from the next place in the chain, not necessarily the parent class. A class method can call super and reach the module it included, which is how a class wraps a mixin’s behaviour without copying it.

A module's instance variables belong to whichever object includes it, so two classes that include the same module do not share state through it. Including a module that calls a method the class does not define fails only when that method is called, not when the module is included — the error arrives later than you would like. And two mixins that define the same method are resolved by the order of inclusion: the last one included wins, which is easy to forget when the includes are pages apart.

Mixed in, and named

module Greeting
  def greet
    "Hello from a #{self.class}"
  end
end

module Loud
  def shout(text)
    "#{text.upcase}!"
  end
end

class Robot
  include Greeting
  include Loud
end

robot = Robot.new
puts robot.greet
puts robot.shout('beep')

# Include Enumerable, define each, and the family arrives.
class Playlist
  include Enumerable

  def initialize(songs)
    @songs = songs
  end

  def each
    @songs.each { |song| yield song }
  end

  def longest
    max_by { |song| song.length }
  end
end

playlist = Playlist.new(%w[Intro Verse Chorus Bridge])
p playlist.map { |song| song.upcase }
p playlist.select { |song| song.length > 5 }
p playlist.sort_by { |song| song.length }
puts playlist.longest
puts playlist.include?('Verse')

# A namespace is a surname.
module Shop
  TAX_RATE = 0.2

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

    def to_s
      "item: #{@name}"
    end
  end
end

puts Shop::Item.new('desk')
puts Shop::TAX_RATE

A reading list that behaves like a collection

Run it and notice that the reading list was never given map or select — they arrived from Enumerable. Then add a module method of your own, mix it in, and call it on the list.

Output
Click "Run" to execute your code

The test for whether something should be a mixin: can it describe several unrelated classes without claiming to be their ancestor? A thing that can be sorted, saved or measured is a mixin; a thing that is a kind of something is a class.

Try these next

  • Write a Describable module with a describe method, include it in two unrelated classes, and call it on both.
  • Use extend to add a helper to one object only, and show that a second object does not have it.
  • Build Workshop::Tool and Garden::Tool, and print both class names to prove the namespaces.

One price rule, two kinds of thing

Run it and compare the two totals. Then add a third class that includes the same module, and a namespace with a constant of its own.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What happens if you call new on a module?