Polyglot
Languages Ruby iterators
Ruby § iterators

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:

ConceptDescription
EnumerableA module — mixes in iteration methods to a class.
EnumeratorA 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 each as the foundation — implement it once, gain everything.
  • Include Enumerable in custom collection classes.
  • Use map, select, reduce for transformations.
  • Use find/detect for first-match lookups.
  • Use any?/all?/none?/one? for boolean tests.
  • Use group_by, partition, chunk_while for substantial grouping.
  • Use tally (Ruby 2.7+) for counting.
  • Use each_with_object for accumulator patterns.
  • Use lazy for substantial or infinite sequences.
  • Use each_slice/each_cons for windowing.
  • Use to_h with a block (Ruby 2.6+) for substantial hash construction.
  • Use flat_map over map(...).flatten(1).
  • Return Enumerator from custom each when 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.