Skip to content

Ruby Mixins — Comparable Enumerable and Custom Mixin Patterns Explained

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Ruby Mixins. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby mixins use modules to share behavior across unrelated classes, with Comparable providing comparison operators via <=> and Enumerable providing iteration methods via each.

What You'll Learn

  • Using Comparable mixin for comparison logic
  • Leveraging Enumerable for powerful collection methods
  • Creating custom mixins for shared behavior
  • The contract pattern — what methods your class must define

Why It Matters

Mixins are Ruby's answer to multiple inheritance without the complexity. Durga Antivirus Pro uses mixins for shared scan strategies (Comparable for threat levels, Enumerable for scan results). Doda Browser uses mixins for bookmark collections, history entries, and tab management. Understanding built-in mixins gives you access to dozens of methods just by defining one or two.

Real-World Use

Every Rails model uses Enumerable-like patterns through ActiveRecord collections. Comparable powers sorting in every sort call. Custom mixins organize cross-cutting concerns like logging, Caching, and authorization across unrelated classes.

flowchart LR
    A["Mixins"] --> B["Comparable"]
    B --> C["Enumerable"]
    C --> D["Custom Mixins"]
    D --> E["Duck Typing"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

The Comparable Mixin

By defining <=> (spaceship operator), you get <, <=, ==, >, >=, and between? for free:

class Temperature
  include Comparable

  attr_reader :celsius

  def initialize(celsius)
    @celsius = celsius
  end

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

freezing = Temperature.new(0)
boiling = Temperature.new(100)
room = Temperature.new(22)

puts freezing < boiling    # true
puts room > freezing       # true
puts room == Temperature.new(22)  # true
puts freezing.between?(room, boiling)  # false
puts [boiling, freezing, room].sort.map(&:celsius).inspect
# [0, 22, 100]

What Comparable Gives You

Define <=> and automatically get:

  • < less than
  • <= less than or equal
  • == equal
  • > greater than
  • >= greater than or equal
  • between? range check
  • clamp (Ruby 2.4+) bound between two values

The Enumerable Mixin

By defining each, you get map, select, reduce, sort, and 50+ methods:

class Playlist
  include Enumerable

  def initialize
    @songs = []
  end

  def add_song(song)
    @songs << song
  end

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

playlist = Playlist.new
playlist.add_song("Bohemian Rhapsody")
playlist.add_song("Stairway to Heaven")
playlist.add_song("Hotel California")

# All these come from Enumerable
puts playlist.map(&:upcase).inspect
# ["BOHEMIAN RHAPSODY", "STAIRWAY TO HEAVEN", "HOTEL CALIFORNIA"]

puts playlist.select { |s| s.start_with?("H") }.inspect
# ["Hotel California"]

puts playlist.sort.inspect
# ["Bohemian Rhapsody", "Hotel California", "Stairway to Heaven"]

Enumerable Methods You Get

class RangeCollection
  include Enumerable

  def initialize(min, max)
    @min = min
    @max = max
  end

  def each
    (@min..@max).each { |n| yield n }
  end
end

rc = RangeCollection.new(1, 5)

puts rc.map { |n| n * 2 }.inspect         # [2, 4, 6, 8, 10]
puts rc.select(&:even?).inspect           # [2, 4]
puts rc.reduce(:+)                        # 15
puts rc.any? { |n| n > 3 }               # true
puts rc.count                             # 5
puts rc.first(3).inspect                  # [1, 2, 3]
puts rc.group_by { |n| n.even? ? :even : :odd }.inspect
# {:odd=>[1, 3, 5], :even=>[2, 4]}

Custom Mixin Example: Taggable

module Taggable
  def self.included(base)
    base.extend(ClassMethods)
  end

  def tags
    @tags ||= []
  end

  def add_tag(tag)
    tags << tag unless tags.include?(tag)
  end

  def remove_tag(tag)
    tags.delete(tag)
  end

  def tagged_with?(tag)
    tags.include?(tag)
  end

  module ClassMethods
    def all_tagged_with(tag)
      all.select { |item| item.tagged_with?(tag) }
    end
  end
end

class Article
  include Taggable

  attr_reader :title

  def initialize(title)
    @title = title
  end

  def self.all
    @all ||= []
  end

  def self.create(title)
    new(title).tap { |a| all << a }
  end
end

ruby_article = Article.create("Ruby Mixins")
rails_article = Article.create("Rails Guides")
ruby_article.add_tag("ruby")
ruby_article.add_tag("programming")
rails_article.add_tag("rails")
rails_article.add_tag("ruby")

puts Article.all_tagged_with("ruby").map(&:title).inspect
# ["Ruby Mixins", "Rails Guides"]

Custom Mixin Example: Serializable

module Serializable
  def to_csv
    instance_variables.map { |var| instance_variable_get(var) }.join(",")
  end

  def to_hash
    instance_variables.each_with_object({}) do |var, hash|
      hash[var.to_s.sub("@", "")] = instance_variable_get(var)
    end
  end

  def display
    to_hash.each { |key, value| puts "#{key}: #{value}" }
  end
end

class Product
  include Serializable

  def initialize(name, price, quantity)
    @name = name
    @price = price
    @quantity = quantity
  end
end

p = Product.new("Widget", 9.99, 100)
puts p.to_csv
puts p.to_hash.inspect
p.display

The Enumerable Contract

To use Enumerable, your class must define each. Enumerable handles the rest:

class Fibonacci
  include Enumerable

  def initialize(limit)
    @limit = limit
  end

  def each
    a, b = 0, 1
    while a <= @limit
      yield a
      a, b = b, a + b
    end
  end
end

fib = Fibonacci.new(100)
puts fib.take(10).inspect  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
puts fib.select(&:even?).inspect  # [0, 2, 8, 34]

The Comparable Contract

Define <=> and get all comparison operators:

class Person
  include Comparable
  attr_reader :name, :age

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

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

people = [
  Person.new("Alice", 30),
  Person.new("Bob", 25),
  Person.new("Charlie", 35)
]

sorted = people.sort
puts sorted.map(&:name).inspect  # ["Bob", "Alice", "Charlie"]

Mixin Composition

Mixins can include other mixins:

module Identifiable
  def id
    object_id
  end
end

module Trackable
  include Identifiable

  def created_at
    @created_at ||= Time.now
  end
end

class Record
  include Trackable
end

r = Record.new
puts r.id           # Some number
puts r.created_at   # Current time

Common Mistakes

1. Forgetting to Define the Required Method

class MyCollection
  include Enumerable
  # Forgot to define each!
end

mc = MyCollection.new
# mc.map { |x| x }  # NoMethodError — each undefined

2. Not Including Comparable Properly

class Temperature
  include Comparable
  attr_reader :celsius

  def initialize(celsius)
    @celsius = celsius
  end

  # Forgot to define <=> !
end

t1 = Temperature.new(20)
t2 = Temperature.new(30)
# t1 < t2  # NoMethodError — <=> not defined

3. Using Include Instead of Extend for Class Methods

module Finder
  def find(id)
    "Found #{id}"
  end
end

class Repository
  include Finder  # Should be extend Finder
end

# Repository.find(1)  # NoMethodError

4. Mutating Objects During Enumerable Operations

class BadCollection
  include Enumerable

  def initialize(items)
    @items = items
  end

  def each(&block)
    @items.each(&block)
    @items.clear  # Modifying during iteration!
  end
end

5. Not Using Enumerable for Custom Collections

Many Ruby developers write manual loops when Enumerable would give them 50+ methods for free by defining just each.

Practice Questions

1. What method must you define to use Comparable?

You must define <=> (the spaceship operator) that returns -1, 0, or 1 for less than, equal, or greater than. Comparable then provides <, <=, ==, >, >=, and between?.

2. What method must you define to use Enumerable?

You must define each that yields elements one at a time. Enumerable then provides map, select, reduce, sort, and 50+ other methods.

3. Can a class include multiple mixins?

Yes. Ruby classes can include any number of modules. Method lookup searches included modules in reverse order of inclusion (last included wins).

4. How do you add both instance and class methods with a single module?

Use the self.included hook: def self.included(base); base.extend(ClassMethods); end. Then define module ClassMethods inside your module.

Challenge: Create an Enumerable::Comparable TemperatureLogger class that stores temperature readings and provides both enumeration (all readings) and comparison (between readings).

Solution
class TemperatureLogger
  include Enumerable
  include Comparable

  attr_reader :readings

  def initialize
    @readings = []
  end

  def add(celsius)
    @readings << celsius
  end

  # For Enumerable
  def each(&block)
    @readings.each(&block)
  end

  # For Comparable (compare by average)
  def <=>(other)
    average <=> other.average
  end

  def average
    return 0 if @readings.empty?
    @readings.sum / @readings.size.to_f
  end

  def range
    @readings.min..@readings.max if @readings.any?
  end
end

today = TemperatureLogger.new
today.add(22)
today.add(25)
today.add(20)

yesterday = TemperatureLogger.new
yesterday.add(18)
yesterday.add(21)

puts "Today avg: #{today.average.round(1)}"
puts "Yesterday avg: #{yesterday.average.round(1)}"
puts "Today warmer: #{today > yesterday}"
puts "All readings: #{today.sort.inspect}"
puts "Above 21: #{today.select { |r| r > 21 }.inspect}"

Expected output:

Today avg: 22.3
Yesterday avg: 19.5
Today warmer: true
All readings: [20, 22, 25]
Above 21: [22, 25]

FAQ

{{< faq question="What's the difference between a mixin and inheritance?" >}} Inheritance is an "is-a" relationship (Dog is an Animal). Mixins add shared behavior to any class regardless of hierarchy. A class can inherit from only one parent but include many modules. {{< /faq >}}

{{< faq question="Can I use Enumerable with hashes?" >}} Yes. Hash includes Enumerable and defines each to yield key-value pairs. Methods like map yield arrays of [key, value], while each_key and each_value are direct. {{< /faq >}}

{{< faq question="How do I sort by multiple criteria with Comparable?" >}} Define <=> to compare multiple attributes: age <=> other.age then name <=> other.name if ages are equal. Use nonzero? or array comparison: [age, name] <=> [other.age, other.name]. {{< /faq >}}

{{< faq question="What happens if a class includes two modules with the same method?" >> The last included module's method wins. Ruby's method lookup searches the class, then included modules in reverse order. Use super in the module to call the next implementation. {{< /faq >}}

{{< faq question="Can I remove a mixin after including it?" >}} No. Ruby doesn't support removing mixins at runtime. Once included, the module is in the ancestor chain for that class. You can undefine specific methods but you can't remove the module itself. {{< /faq >}}

Try It Yourself

# mixins_demo.rb

module Stats
  def self.included(base)
    base.extend(ClassStats)
  end

  def sum
    reduce(0, :+)
  end

  def average
    sum / count.to_f
  end

  module ClassStats
    def description
      "A collection with Stats mixin"
    end
  end
end

class ScoreCollection
  include Enumerable
  include Stats

  def initialize(scores = [])
    @scores = scores
  end

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

scores = ScoreCollection.new([85, 92, 78, 95, 88])
puts ScoreCollection.description
puts "Sum: #{scores.sum}"
puts "Avg: #{scores.average.round(1)}"
puts "Max: #{scores.max}"
puts "Min: #{scores.min}"
puts "Passing: #{scores.count { |s| s >= 80 }}"

Expected output:

A collection with Stats mixin
Sum: 438
Avg: 87.6
Max: 95
Min: 78
Passing: 4

What's Next

Now that you understand mixins, learn about duck typing — Ruby's philosophy of programming to interfaces rather than types.

Topic Description Link
Ruby Duck Typing respond_to?, method_missing {{< ref "15-duck-typing" >}}
Ruby Open Classes Monkey patching, refinements {{< ref "16-open-classes" >}}
Python Enumerate Compare Python's iteration tools Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro