Ruby Mixins — Comparable Enumerable and Custom Mixin Patterns Explained
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 equalbetween?range checkclamp(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