Comparable and Enumerable Mixins

Published by Programming Line in Ruby. Last updated .

A mixin is Ruby’s way of sharing a bundle of behavior across unrelated classes without inheritance, and Comparable and Enumerable are the two mixins you will reach for constantly. Both work on the same trick: you implement one small, well-defined method on your class, and the module hands you a whole toolbox of higher-level methods for free — sorting, clamping, searching, mapping, filtering, and more. Comparable needs a spaceship operator method and gives you comparison operators, between?, and clamp. Enumerable needs an each method and gives you map, select, sort, reduce, and dozens more. Understanding these two mixins is understanding a huge slice of idiomatic Ruby.

Overview: How Mixins Work

In Ruby, a module is a bag of methods that cannot be instantiated on its own. When a class calls include SomeModule, Ruby inserts that module into the class’s ancestor chain — the ordered list of places Ruby searches when you call a method on an object. You can see this chain yourself with SomeClass.ancestors. The search order is: the object’s singleton class, then its own class, then any included modules (most recently included first), then the superclass and its modules, all the way up to Object, Kernel, and BasicObject. This is exactly how Comparable and Enumerable plug in: the module’s methods sit in the chain right above your class, so calling my_object.sort finds Enumerable#sort even though you never wrote a sort method yourself.

Both mixins follow the same design idea, sometimes called a template method pattern: the module provides dozens of derived methods, all implemented in terms of one primitive method that you supply.

  • Comparable requires your class to define <=> (the "spaceship operator"). It must return -1 if self is less than the argument, 0 if they are equal, 1 if self is greater, and nil if the two are not comparable at all. From that single method, Comparable derives <, <=, ==, >, >=, between?, and clamp.
  • Enumerable requires your class to define each, yielding one element at a time. From that single method, Enumerable derives over fifty methods, including map, select, reject, reduce, sort, sort_by, min, max, min_by, max_by, count, find, any?, all?, none?, group_by, sum, first, and to_a.

This is also why Ruby feels so consistent across different classes: Array, Hash, Range, and Struct all include Enumerable under the hood, and Integer, String, and Time all include Comparable. Once you understand the mixin, you understand why (1..10).select and a call to select on your own custom collection behave identically — they are the exact same Enumerable#select method running on top of two different each implementations.

Syntax

The general shape for each mixin looks like this:

class ClassName
  include Comparable

  def <=>(other)
    # compare self to other, return -1, 0, 1, or nil
  end
end

class OtherClassName
  include Enumerable

  def each
    # yield each element to the block
  end
end
Mixin Method you must define What it must return Sample of methods you get for free
Comparable <=>(other) -1, 0, 1, or nil <, <=, ==, >, >=, between?, clamp
Enumerable each (yields elements) the receiver itself, or an Enumerator if no block is given map, select, reject, reduce, sort, sort_by, min, max, group_by, sum, find, any?, all?, first, to_a

Examples

Example 1: Comparable for a value-like object

Any class that represents a single comparable value — a temperature, a version number, a card rank — is a great fit for Comparable. You only write <=>; every comparison operator, between?, and clamp come along automatically.

class Temperature
  include Comparable
  attr_reader :degrees

  def initialize(degrees)
    @degrees = degrees
  end

  def <=>(other)
    degrees <=> other.degrees
  end

  def to_s
    "#{degrees}F"
  end
end

freezing = Temperature.new(32)
boiling = Temperature.new(212)
room = Temperature.new(72)

puts freezing < boiling
puts room.between?(freezing, boiling)

temps = [boiling, freezing, room]
puts temps.sort.map(&:to_s).join(", ")
puts temps.max
puts room.clamp(freezing, Temperature.new(50))

Output:

true
true
32F, 72F, 212F
212F
50F

Every one of those calls — <, between?, sort, max, clamp — is a method Comparable supplies. Ruby only ever calls your <=> method underneath. clamp(min, max) is worth noting specially: it returns min if the receiver is less than min, max if it is greater than max, and the receiver itself otherwise — here room (72) is above the given max of 50, so it gets pulled down to 50.

Example 2: Enumerable for a custom collection

Any class that wraps a group of items — a todo list, a deck of cards, a request queue — is a great fit for Enumerable. You only write each; map, select, count, any?, and dozens of others come along automatically.

class TodoList
  include Enumerable

  def initialize
    @tasks = []
  end

  def add(name, done: false)
    @tasks << { name: name, done: done }
    self
  end

  def each
    return enum_for(:each) unless block_given?

    @tasks.each { |task| yield task }
  end
end

list = TodoList.new
list.add("Buy milk").add("Write Ruby lesson", done: true).add("Walk dog")

puts list.count
puts list.map { |task| task[:name] }.join(", ")
puts list.select { |task| task[:done] }.map { |task| task[:name] }.join(", ")
puts list.any? { |task| task[:done] }
puts list.first[:name]

Output:

3
Buy milk, Write Ruby lesson, Walk dog
Write Ruby lesson
true
Buy milk

TodoList never defines count, map, select, any?, or first — all five come from Enumerable, and all five work by repeatedly calling the one each method you wrote. Notice add returns self, which is what makes the chained list.add(...).add(...).add(...) call possible.

Example 3: Combining both mixins

In real code, Comparable and Enumerable usually show up together: a container class includes Enumerable to be iterable, and the elements inside it include Comparable so the container can be sorted.

class Song
  include Comparable
  attr_reader :title, :duration

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

  def <=>(other)
    duration <=> other.duration
  end

  def to_s
    "#{title} (#{duration}s)"
  end
end

class Playlist
  include Enumerable

  def initialize(*songs)
    @songs = songs
  end

  def each(&block)
    @songs.each(&block)
  end
end

playlist = Playlist.new(
  Song.new("Intro", 45),
  Song.new("Chorus Loop", 210),
  Song.new("Outro", 30)
)

puts playlist.sort.map(&:to_s).join(" | ")
puts playlist.max
puts playlist.sum(&:duration)

grouped = playlist.group_by { |song| song.duration > 60 ? :long : :short }
grouped.each { |type, songs| puts "#{type}: #{songs.map(&:title).join(', ')}" }

Output:

Outro (30s) | Intro (45s) | Chorus Loop (210s)
Chorus Loop (210s)
285
short: Intro, Outro
long: Chorus Loop

Playlist#sort and Playlist#max both come from Enumerable, but they only work because each Song inside the playlist supplies its own <=> via Comparable. Two different mixins, two different classes, working together seamlessly.

How It Works Step by Step

Walk through playlist.sort from Example 3:

  1. Ruby looks up sort on playlist. Playlist doesn’t define it, but Enumerable is in its ancestor chain, so Enumerable#sort runs.
  2. Enumerable#sort calls each internally to pull every element into a plain array.
  3. It sorts that array by comparing elements pairwise with their own <=> method — here, Song#<=>, supplied because Song includes Comparable.
  4. The sorted array is returned. Playlist itself never implements any sorting logic at all.

Now walk through what happens when each is called without a block, as in list.each from Example 2:

  1. Ruby executes your each method body as normal.
  2. It reaches return enum_for(:each) unless block_given?, sees no block was passed, and exits immediately, handing back a lazy Enumerator bound to the object and the method name :each. Nothing has iterated yet.
  3. Calling .next on that Enumerator resumes execution of your each method behind the scenes, pausing right after the first yield and handing back that value.
  4. Without that guard line, each would run straight into its internal yield with no block attached, and Ruby raises LocalJumpError: no block given (yield) instead of returning something useful.

Common Mistakes

Mistake 1: Including Comparable without defining <=>

include Comparable by itself does nothing useful — every method it provides delegates to <=>, and if your class doesn’t define one, Ruby falls back to Object#<=>, which only returns 0 for identical objects and nil otherwise. Any comparison then raises.

class Box
  include Comparable
end

small = Box.new
large = Box.new
small < large
# => NoMethodError: comparison of Box with Box failed (Box doesn't define <=>)

The fix is to give the class an attribute to compare by and implement <=> in terms of it:

class Box
  include Comparable
  attr_reader :volume

  def initialize(volume)
    @volume = volume
  end

  def <=>(other)
    volume <=> other.volume
  end
end

small = Box.new(10)
large = Box.new(100)

puts small < large

Output:

true

Mistake 2: Forgetting the block_given? guard in each

Many Enumerable methods, and useful patterns like .each.with_index, rely on being able to call your each without a block to get back an Enumerator. If your each jumps straight to yield, calling it without a block crashes instead of degrading gracefully.

class Numbers
  include Enumerable

  def initialize(*nums)
    @nums = nums
  end

  def each
    @nums.each { |n| yield n }
  end
end

nums = Numbers.new(3, 1, 2)
enum = nums.each
puts enum.next
# => LocalJumpError: no block given (yield), because `each` has no
#    block_given? guard and jumps straight to `yield`

Adding the guard fixes it and restores normal Enumerable behavior:

class Numbers
  include Enumerable

  def initialize(*nums)
    @nums = nums
  end

  def each
    return enum_for(:each) unless block_given?

    @nums.each { |n| yield n }
  end
end

nums = Numbers.new(3, 1, 2)
enum = nums.each
puts enum.next
puts enum.next
puts nums.sort.inspect

Output:

3
1
[1, 2, 3]

Mistake 3: Forgetting that Comparable silently redefines ==

Once you include Comparable, == is no longer Ruby’s default identity check — it becomes "does <=> return 0?" If your <=> only looks at one attribute, two objects that differ in every other way can compare as equal.

class Money
  include Comparable
  attr_reader :cents, :currency

  def initialize(cents, currency)
    @cents = cents
    @currency = currency
  end

  def <=>(other)
    cents <=> other.cents
  end
end

usd = Money.new(500, "USD")
eur = Money.new(500, "EUR")

puts usd == eur

Output:

true

That true is a bug: 500 US cents and 500 Euro cents are not the same amount of money, but <=> never looked at currency. The fix is to make <=> return nil when the objects aren’t truly comparable — Comparable#== treats a nil result as "not equal" rather than raising:

class Money
  include Comparable
  attr_reader :cents, :currency

  def initialize(cents, currency)
    @cents = cents
    @currency = currency
  end

  def <=>(other)
    return nil unless currency == other.currency

    cents <=> other.cents
  end
end

usd = Money.new(500, "USD")
eur = Money.new(500, "EUR")
usd2 = Money.new(300, "USD")

puts usd == eur
puts usd > usd2

Output:

false
true

Best Practices

  • Always make <=> return -1, 0, 1, or nil — never raise an exception inside it and never return true/false.
  • Always guard each with return enum_for(:each) unless block_given? so your object plays well with Enumerator-based code like .each.with_index or .lazy.
  • Only include Comparable when a true, total ordering makes sense for the class, and remember it silently redefines ==; if two objects should be equal only when truly identical, keep <=> narrow and return nil for anything it can’t honestly compare.
  • Only include Enumerable when your class genuinely represents a collection; if you’re just wrapping an existing Array or Hash, delegating each to it (as Playlist does above) is simpler than reinventing iteration.
  • For multi-key comparisons, compare arrays instead of chaining conditionals: [last_name, first_name] <=> [other.last_name, other.first_name] reads cleanly and Array’s own <=> handles the tie-breaking.
  • Prefer sort_by, min_by, and max_by over sort, min, and max with a custom <=> when you’re comparing by a computed key — they compute the key once per element instead of recomputing it on every comparison.
  • Remember that <, >, between?, and friends raise ArgumentError when <=> returns nil, while == just quietly returns false — don’t assume the two behave the same way on incomparable objects.

Practice Exercises

  1. Write a Card class with rank (2–14) and suit attributes that includes Comparable, ordering strictly by rank. Build a five-card hand as an array of Card objects, sort it, and print the highest card. Hint: hand.max should give the same card as hand.sort.last.
  2. Write a Bookshelf class that wraps an array of book records (use a Struct.new(:title, :pages, :year)) and includes Enumerable by delegating each to the internal array. Using only Enumerable methods, compute the total page count with sum, find the newest book with max_by, and list every book published before 2000 with select.
  3. The <=> method below is broken:
    def <=>(other)
      age > other.age  # WRONG: returns true/false, not -1/0/1
    end
    

    Explain why this breaks sort and < even though it looks reasonable, then rewrite it correctly using the spaceship operator on age instead.

Summary

  • Comparable and Enumerable are mixins: modules included into a class that add methods to its ancestor chain.
  • Comparable needs exactly one method, <=>, returning -1, 0, 1, or nil; in exchange it provides <, <=, ==, >, >=, between?, and clamp.
  • Enumerable needs exactly one method, each, that yields elements; in exchange it provides map, select, sort, reduce, group_by, sum, and dozens more.
  • Guard custom each methods with return enum_for(:each) unless block_given? so they can be called without a block.
  • Including Comparable redefines == to mean "<=> returns 0" — make sure that matches what equality should really mean for your class.
  • Container classes typically include Enumerable while the elements they hold include Comparable, and the two work together automatically.