Iterators and the Enumerable Module in Ruby: each, map, select, reduce, and Collection Methods Guide

Iterators and the Enumerable Module in Ruby

Iterators and the Enumerable Module in Ruby

If there’s one thing that made me fall in love with Ruby early on, it was the moment I understood the Enumerable module properly. Before that, I was writing for loops out of habit from other languages. After it clicked, I realized Ruby had quietly given me a whole vocabulary for expressing what I wanted done with a collection, instead of manually writing how to loop through it. This article is my attempt to explain iterators and Enumerable the way I wish someone had explained them to me — from the basics of each all the way to writing your own enumerable classes and understanding what’s happening internally.

What Is an Iterator, Really?

In Ruby, an iterator is just a method that yields control back to a block, one element at a time. The classic example is each:

fruits = ["apple", "banana", "cherry"]

fruits.each do |fruit|
  puts fruit
end

Output:

apple
banana
cherry

each doesn’t return anything meaningful for chaining — it returns the original array. Its whole job is running the block once per element, for side effects like printing. This is the foundation everything else in Enumerable builds on top of.

The Enumerable Module

Here’s the part that took me a while to fully appreciate: Array, Hash, Range, and many other classes don’t each implement map, select, reduce, and dozens of other methods separately. They implement one method — each — and then include Enumerable, which gives them around 50 additional methods for free, all built on top of that single each.

puts Array.ancestors.include?(Enumerable)  # true
puts Hash.ancestors.include?(Enumerable)   # true
puts Range.ancestors.include?(Enumerable)  # true

This is one of the cleanest examples of Ruby’s design philosophy: define one primitive operation, mix in a module, and inherit a rich, expressive API.

map: Transforming Collections

map (aliased as collect) runs the block on every element and returns a new array built from the block’s return values:

numbers = [1, 2, 3, 4, 5]
squared = numbers.map { |n| n ** 2 }
puts squared.inspect

Output:

[1, 4, 9, 16, 25]

Notice numbers itself is untouched — map doesn’t mutate the original array unless you use the bang version, map!, which replaces the array’s contents in place:

numbers.map! { |n| n * 10 }
puts numbers.inspect  # [10, 20, 30, 40, 50]

I generally avoid bang methods unless I have a specific reason (like avoiding an allocation in a hot loop), because mutating shared state quietly is a common source of bugs.

select and reject: Filtering Collections

select (aliased filter) keeps elements where the block returns truthy; reject keeps elements where it returns falsy — the exact inverse:

numbers = (1..20).to_a

evens = numbers.select { |n| n.even? }
odds  = numbers.reject { |n| n.even? }

puts evens.inspect
puts odds.inspect

Output:

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

There’s also find (aliased detect), which returns the first matching element instead of all of them:

first_multiple_of_seven = numbers.find { |n| n % 7 == 0 }
puts first_multiple_of_seven  # 7

reduce / inject: Folding a Collection into One Value

reduce (aliased inject) is the one that confuses people longest, but it’s the most powerful of the bunch — it folds a whole collection down into a single accumulated value.

numbers = [1, 2, 3, 4, 5]

sum = numbers.reduce(0) { |accumulator, n| accumulator + n }
puts sum  # 15

The first argument (0) is the starting value of the accumulator. Each iteration, the block’s return value becomes the new accumulator for the next iteration. You can also pass a symbol directly for simple operations, skipping the explicit block:

sum = numbers.reduce(:+)
product = numbers.reduce(1, :*)
puts sum      # 15
puts product  # 120

reduce isn’t limited to numbers — it’s genuinely general-purpose. Here’s building a word-frequency hash:

words = %w[ruby is fun ruby is powerful ruby is elegant]

frequency = words.reduce(Hash.new(0)) do |counts, word|
  counts[word] += 1
  counts
end

puts frequency.inspect

Output:

{"ruby"=>3, "is"=>3, "fun"=>1, "powerful"=>1, "elegant"=>1}

Other Enumerable Methods Worth Knowing

A handful of others I use constantly:

numbers = [5, 3, 8, 1, 9, 2]

puts numbers.sort.inspect          # [1, 2, 3, 5, 8, 9]
puts numbers.sort { |a, b| b <=> a }.inspect  # descending
puts numbers.min                   # 1
puts numbers.max                   # 9
puts numbers.sum                   # 28
puts numbers.count { |n| n > 3 }   # 3
puts numbers.any? { |n| n > 8 }    # true
puts numbers.all? { |n| n > 0 }    # true
puts numbers.none? { |n| n > 100 } # true
puts numbers.group_by { |n| n.even? ? :even : :odd }.inspect
puts numbers.each_with_index.to_a.inspect
puts numbers.each_with_object([]) { |n, arr| arr << n * 2 }.inspect

Output:

[1, 2, 3, 5, 8, 9]
[9, 8, 5, 3, 2, 1]
1
9
28
3
true
true
true
{:odd=>[5, 3, 1, 9], :even=>[8, 2]}
[[5, 0], [3, 1], [8, 2], [1, 3], [9, 4], [2, 5]]
[10, 6, 16, 2, 18, 4]

each_with_object is my preferred alternative to reduce when the accumulator is a mutable object like an array or hash, since you don’t have to remember to return it explicitly at the end of the block.

Writing Your Own Enumerable Class

This is where Enumerable really shows its design. If you define each on your own class and mix in Enumerable, you get map, select, reduce, sort, and everything else automatically.

class Playlist
  include Enumerable

  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
    self
  end

  def each
    return enum_for(:each) unless block_given?
    @songs.each { |song| yield song }
  end
end

playlist = Playlist.new
playlist.add("Bohemian Rhapsody").add("Imagine").add("Hotel California")

puts playlist.map(&:upcase).inspect
puts playlist.select { |s| s.include?("H") }.inspect
puts playlist.sort.inspect
puts playlist.count

Output:

["BOHEMIAN RHAPSODY", "IMAGINE", "HOTEL CALIFORNIA"]
["Bohemian Rhapsody", "Hotel California"]
["Bohemian Rhapsody", "Hotel California", "Imagine"]
3

I only had to write each. Every other method — map, select, sort, count, dozens more — came free from Enumerable, because internally they’re all implemented in terms of repeatedly calling each and collecting results.

Internal Working: Enumerator Objects and Lazy Evaluation

When you call an iterator method without a block, Ruby doesn’t run it immediately — it returns an Enumerator object instead:

enum = [1, 2, 3].map
puts enum.class  # Enumerator

result = enum.each { |n| n * 2 }
puts result.inspect  # [2, 4, 6]

This is what powers the return enum_for(:each) unless block_given? line in the Playlist example above — it lets each work correctly whether or not a block is passed, which Enumerable‘s other methods rely on internally.

Enumerators also support external iteration using next, which under the hood is implemented using Ruby Fibers — lightweight, cooperatively-scheduled coroutines that let the enumerator pause mid-iteration and resume later:

enum = [10, 20, 30].each
puts enum.next  # 10
puts enum.next  # 20
puts enum.next  # 30

For working with very large or infinite sequences, lazy enumerators avoid computing the entire chain eagerly:

lazy_result = (1..Float::INFINITY).lazy
                                   .select { |n| n % 3 == 0 }
                                   .map { |n| n * 2 }
                                   .first(5)

puts lazy_result.inspect

Output:

[6, 12, 18, 24, 30]

Without .lazy, calling select on an infinite range would hang forever trying to build the full filtered array before map even starts. .lazy chains the operations so each value flows through the whole pipeline one at a time, stopping once first(5) has what it needs.

Real-World Applications

I use Enumerable methods constantly for:

Best Practices

  1. Prefer map/select/reduce over manual loops when you’re transforming data — it communicates intent and avoids off-by-one bugs.
  2. Use each when you genuinely only need side effects (printing, logging) — don’t use map and discard its return value just out of habit.
  3. Use each_with_object instead of reduce when accumulating into a mutable collection, for cleaner code.
  4. Reach for .lazy when working with large or infinite sequences, or when you only need the first few results.
  5. Include Enumerable in custom classes that represent collections, rather than reinventing map/select/sort yourself.

Common Mistakes

Debugging Tips

When a chain of Enumerable calls isn’t producing what you expect, break it apart and inspect intermediate results with p after each step rather than debugging the whole chain at once. For custom Enumerable classes, test each in isolation first — if each is wrong, everything built on top of it (map, select, sort, reduce) will be wrong too, since they all delegate to it internally.

Summary

Ruby’s iterators and the Enumerable module are, in my experience, one of the best examples of good API design in any mainstream language: implement each once, mix in a module, and get dozens of expressive, well-tested collection methods for free. Learning to reach for map, select, and reduce instead of manual loops doesn’t just make code shorter — it makes intent clearer, and once you understand how Enumerable is built on each and Enumerator, you can extend that same power to your own custom classes.

References

Exit mobile version