Enumerable
The Enumerable module is one of Ruby’s most distinctive features. Any class that defines each (and includes Enumerable) gains a substantial library of iteration methods: map, select, reject, reduce, find, count, sort, group_by, partition, chunk_while, each_slice, each_cons, lazy, etc. The mechanism admits substantial code reuse — many of these methods derive entirely from each. The principal types implementing Enumerable are Array, Hash, Range, Set, IO (line iteration), and String#each_* variants. The combination — each as the principal customisation point, the substantial derived methods, the Enumerator class for lazy iteration, the lazy adapter for streams — is the substance of Ruby’s iteration surface.
The each foundation
The conventional iterator method:
[1, 2, 3].each { |x| puts x }
{ a: 1, b: 2 }.each { |k, v| puts "#{k}: #{v}" }
(1..5).each { |n| puts n }
"hello".each_char { |c| puts c }
File.foreach("file.txt") { |line| puts line }
The each is the principal iterator; many other methods (map, select, etc.) are derived from each.
For custom classes implementing Enumerable, defining each admits the entire iteration surface:
class TodoList
include Enumerable
def initialize
@items = []
end
def add(item)
@items << item
end
def each(&block)
@items.each(&block)
end
end
list = TodoList.new
list.add("buy milk")
list.add("write code")
# All these now work:
list.map(&:upcase)
list.select { |i| i.include?("milk") }
list.first
list.count
list.sort
list.to_a
list.any? { |i| i.length > 10 }
Transformation: map and friends
[1, 2, 3].map { |n| n * 2 } # [2, 4, 6]
[1, 2, 3].map(&:to_s) # ["1", "2", "3"]
[1, 2, 3].collect { |n| n * 2 } # alias for map
[1, 2, 3].flat_map { |n| [n, n * 2] } # [1, 2, 2, 4, 3, 6]
# = map then flatten by 1 level
[1, 2, 3].map.with_index { |n, i| "#{i}: #{n}" }
# ["0: 1", "1: 2", "2: 3"]
[[1, 2], [3, 4]].flat_map(&:itself) # [1, 2, 3, 4]
The flat_map is a substantial alternative to map(...).flatten(1).
Filtering: select and reject
[1, 2, 3, 4, 5].select { |n| n.even? } # [2, 4]
[1, 2, 3, 4, 5].filter { |n| n.even? } # alias for select (Ruby 2.6+)
[1, 2, 3, 4, 5].reject { |n| n.even? } # [1, 3, 5]
[1, nil, 2, nil, 3].compact # [1, 2, 3] (remove nils)
[1, 2, 2, 3, 3, 3].uniq # [1, 2, 3]
[1, 2, 3].partition { |n| n.even? } # [[2], [1, 3]] (true and false)
# With index:
items.select.with_index { |x, i| i.even? }
Aggregation: reduce
[1, 2, 3].reduce(0) { |sum, n| sum + n } # 6
[1, 2, 3].reduce(:+) # 6 (symbol shorthand)
[1, 2, 3].reduce(0, :+) # 6
[1, 2, 3].sum # 6 (built-in)
[1, 2, 3].inject(:*) # 6 (alias for reduce)
[3, 1, 4, 1, 5, 9].max # 9
[3, 1, 4, 1, 5, 9].min # 1
[1, 2, 3].reduce { |a, b| a + b } # 6 (no initial; uses first element)
The reduce (alias inject) admits substantial accumulation patterns.
Search: find, index, detect
[1, 2, 3, 4].find { |n| n > 2 } # 3
[1, 2, 3, 4].detect { |n| n > 2 } # 3 (alias)
[1, 2, 3, 4].find_index { |n| n > 2 } # 2 (the index)
[1, 2, 3, 4].index(3) # 2
[1, 2, 3].include?(2) # true
[1, 2, 3].member?(2) # alias
# All matches:
[1, 2, 3, 4].select { |n| n > 2 } # [3, 4]
Boolean tests: any?, all?, none?, one?
[1, 2, 3].any? { |n| n > 2 } # true
[1, 2, 3].all? { |n| n > 0 } # true
[1, 2, 3].none? { |n| n > 5 } # true
[1, 2, 3].one? { |n| n == 2 } # true
# With argument (uses ===):
[1, "two", :three].any?(String) # true
[1, 2, 3].all?(Integer) # true
The block-less forms (with an argument) use === for matching.
Sorting
[3, 1, 4, 1, 5].sort # [1, 1, 3, 4, 5]
[3, 1, 4, 1, 5].sort { |a, b| b <=> a } # descending: [5, 4, 3, 1, 1]
people.sort_by(&:age) # by attribute
people.sort_by { |p| [p.role, p.age] } # multi-key
[3, 1, 4].min # 1
[3, 1, 4].max # 4
[3, 1, 4].minmax # [1, 4]
[3, 1, 4].max_by(&:itself) # 4
[1, 2, 3, 4, 5].min_by { |n| (n - 3).abs } # 3 (closest to 3)
The sort mutates returns a new array; sort_by is more efficient for substantial computations.
Grouping and chunking
[1, 2, 3, 4, 5, 6].group_by { |n| n % 3 }
# {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}
people.group_by(&:role)
# {"admin"=>[Alice], "user"=>[Bob, Charlie]}
# Tally (count by value):
[1, 1, 2, 2, 2, 3].tally # {1=>2, 2=>3, 3=>1}
# Chunk consecutive elements:
[1, 1, 2, 2, 3].chunk_while { |a, b| a == b }.to_a
# [[1, 1], [2, 2], [3]]
[1, 2, 3, 5, 6].chunk_while { |a, b| a + 1 == b }.to_a
# [[1, 2, 3], [5, 6]] (consecutive integers)
# Slicing:
[1, 2, 3, 4, 5].each_slice(2).to_a # [[1, 2], [3, 4], [5]]
[1, 2, 3, 4, 5].each_cons(3).to_a # [[1,2,3], [2,3,4], [3,4,5]]
# (overlapping windows)
Counting
[1, 2, 3].count # 3
[1, 2, 3, 2, 1].count(2) # 2 (count of 2)
[1, 2, 3, 4].count { |n| n.even? } # 2
[1, 2, 3].size # 3 (alias)
[1, 2, 3].length # 3 (alias)
Combining: zip and chain
[1, 2, 3].zip([:a, :b, :c]) # [[1,:a], [2,:b], [3,:c]]
[1, 2, 3].zip([:a, :b]) # [[1,:a], [2,:b], [3, nil]]
[1, 2].zip([:a, :b, :c]) # [[1,:a], [2,:b]] (truncated)
[1, 2].zip([3, 4], [5, 6]) # [[1, 3, 5], [2, 4, 6]]
# Chain (Ruby 2.6+):
[1, 2].chain([3, 4]) # Enumerator: 1, 2, 3, 4
[1, 2].chain([3, 4]).to_a # [1, 2, 3, 4]
each_with_index and each_with_object
[10, 20, 30].each_with_index do |x, i|
puts "[#{i}] #{x}"
end
# each_with_object — for accumulator patterns:
result = [1, 2, 3].each_with_object({}) do |x, h|
h[x] = x * x
end
# {1=>1, 2=>4, 3=>9}
# vs reduce:
result = [1, 2, 3].reduce({}) do |h, x|
h[x] = x * x
h # must return the accumulator
end
# each_with_object is conventionally clearer when mutation is intended
lazy enumerators
The lazy admits “compute on demand”:
# Eager (constructs intermediate arrays):
result = (1..Float::INFINITY).map { |n| n * n }.first(5)
# never returns!
# Lazy:
result = (1..Float::INFINITY).lazy.map { |n| n * n }.first(5)
# [1, 4, 9, 16, 25]
# Lazy chain:
result = (1..).lazy
.map { |n| n * n }
.select { |n| n.even? }
.first(10)
# [4, 16, 36, ...]
The mechanism admits substantial efficiency for substantial pipelines, particularly when only a prefix is needed.
The force (or to_a) materialises a lazy enumerator:
(1..1000).lazy.map { |n| n * 2 }.force.first(3) # [2, 4, 6]
each_entry and yields
For methods that may yield differently-shaped values:
class Container
def each
yield 1
yield 2, 3 # yield two values
end
end
c = Container.new
c.each_entry { |x| p x }
# 1
# [2, 3]
Returning enumerators from each
The conventional discipline returns an Enumerator when each is called without a block:
class TodoList
include Enumerable
def each
return to_enum(:each) unless block_given?
@items.each { |item| yield item }
end
end
list.each # Enumerator
list.each.lazy.map { ... }.first(5)
list.each.with_index { |item, i| puts "#{i}: #{item}" }
The to_enum (alias enum_for) returns an Enumerator admitting subsequent chaining.
Common patterns
Pipeline transformation
result = users
.select(&:active?)
.map(&:profile)
.compact
.sort_by(&:created_at)
.first(10)
The pattern is conventional for substantial functional-style processing.
Counting unique values
distinct = items.uniq.count
counts = items.tally
top_5 = counts.sort_by { |_, n| -n }.first(5)
Group and aggregate
totals = items.group_by(&:category)
.transform_values { |xs| xs.sum(&:amount) }
# Or:
totals = items.each_with_object(Hash.new(0)) do |item, h|
h[item.category] += item.amount
end
Top N by criterion
top_3_oldest = people.max_by(3, &:age) # Ruby 2.2+
top_3_youngest = people.min_by(3, &:age)
Partition
adults, minors = people.partition { |p| p.age >= 18 }
Building a hash
# From pairs:
hash = entries.to_h
# With transformation:
by_id = users.to_h { |u| [u.id, u] } # Ruby 2.6+
# Or with each_with_object:
by_id = users.each_with_object({}) do |u, h|
h[u.id] = u
end
Custom enumerable
class Tree
include Enumerable
def initialize(value, children = [])
@value = value
@children = children
end
def each(&block)
yield @value
@children.each { |c| c.each(&block) }
end
end
tree = Tree.new(1, [Tree.new(2), Tree.new(3, [Tree.new(4)])])
tree.to_a # [1, 2, 3, 4] (depth-first)
tree.sum # 10
tree.count # 4
tree.find { |n| n > 2 } # 3
Lazy infinite sequence
fibonacci = Enumerator.new do |y|
a, b = 0, 1
loop do
y << a
a, b = b, a + b
end
end
fibonacci.first(10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
fibonacci.lazy.select(&:even?).first(5)
Slicing for batches
records.each_slice(100) do |batch|
process_batch(batch)
end
The pattern admits substantial batch processing for substantial collections.
Sliding window
prices = [100, 102, 98, 105, 110, 108]
prices.each_cons(3).map { |window| window.sum / 3.0 }
# [100.0, 101.67, 104.33, 107.67]
Flatten with mapping
posts.flat_map(&:comments) # all comments across posts
each_with_object for builders
result = data.each_with_object(Result.new) do |item, r|
r.add(item.transform)
end
map then to_h
config = ENV.map { |k, v| [k.downcase.to_sym, v] }.to_h
# Or directly:
config = ENV.each_with_object({}) do |(k, v), h|
h[k.downcase.to_sym] = v
end
Stream over a file
File.foreach("large.log").lazy
.select { |line| line.include?("ERROR") }
.map { |line| line.split[0] }
.first(100)
The File.foreach returns an Enumerator over lines; lazy admits stream processing.
A note on Enumerable vs Enumerator
The two distinct concepts:
| Concept | Description |
|---|---|
Enumerable | A module — mixes in iteration methods to a class. |
Enumerator | A class — represents a (potentially lazy) iteration sequence. |
Custom classes use Enumerable to gain iteration methods; the methods often return Enumerator instances when called without a block.
[1, 2, 3].class # Array (includes Enumerable)
[1, 2, 3].each.class # Enumerator
[1, 2, 3].lazy.class # Enumerator::Lazy
A note on the conventional discipline
The contemporary Ruby Enumerable advice:
- Use
eachas the foundation — implement it once, gain everything. - Include
Enumerablein custom collection classes. - Use
map,select,reducefor transformations. - Use
find/detectfor first-match lookups. - Use
any?/all?/none?/one?for boolean tests. - Use
group_by,partition,chunk_whilefor substantial grouping. - Use
tally(Ruby 2.7+) for counting. - Use
each_with_objectfor accumulator patterns. - Use
lazyfor substantial or infinite sequences. - Use
each_slice/each_consfor windowing. - Use
to_hwith a block (Ruby 2.6+) for substantial hash construction. - Use
flat_mapovermap(...).flatten(1). - Return
Enumeratorfrom customeachwhen no block is given.
The combination — each as the foundation, the substantial derived methods, lazy evaluation via Enumerator, the conventional method chaining — is the substance of Ruby’s iteration surface. The discipline admits substantial functional-style transformations through the substantial method library; the conventional Ruby discipline is to compose iterator methods rather than write explicit loops.