Custom Iterators & Enumerator
Reviewed & published by Brayan K
A custom iterator is a class that defines an each method and includes Enumerable, instantly gaining map, select, and dozens of other collection methods for free.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll make your own objects iterable, return chainable Enumerator objects, and tame infinite sequences with lazy evaluation.
What You'll Learn in This Lesson
- Defining each so a class can be iterated
- Including Enumerable to get map, select, count free
- Returning an Enumerator when no block is given
- Lazy enumerators for infinite sequences
- Building values with each_with_object
- Numbering items with with_index
1️⃣ each + Enumerable = Free Superpowers
The deal Ruby offers is generous: write a single each method that yields every element, include Enumerable, and your class gains map, select, count, include?, min_by, sort, and roughly fifty more — all implemented in terms of each.
# A class becomes iterable by defining `each` and including Enumerable.
class Playlist
include Enumerable # gives us map, select, count, etc. for free
def initialize(*songs)
@songs = songs
end
# The ONE method you must write: yield each element.
def each
@songs.each { |song| yield song }
end
end
mix = Playlist.new("Intro", "Verse", "Chorus", "Bridge")
# Because we included Enumerable, ALL of these now work:
puts mix.map { |s| s.upcase }.inspect
puts mix.select { |s| s.length > 5 }.inspect
puts mix.count
puts mix.first(2).inspect
puts mix.include?("Chorus")2️⃣ Returning an Enumerator (No Block)
Ruby's own collections do something clever: when you call each with no block, they return an Enumerator instead of looping. That object can be chained (.with_index, .lazy) or stepped through manually with .next. Add return to_enum(:each) unless block_given? to give your class the same power.
# Returning an Enumerator when no block is given makes a method chainable.
class Countdown
include Enumerable
def initialize(from)
@from = from
end
def each
return to_enum(:each) unless block_given? # no block? hand back an Enumerator
n = @from
while n >= 1
yield n
n -= 1
end
end
end
c = Countdown.new(5)
# With a block: behaves like a loop.
c.each { |n| print n, " " }
puts
# Without a block: you get an Enumerator you can chain with.
enum = c.each
puts enum.class
puts enum.with_index(1).map { |n, i| "#{i}:#{n}" }.inspect
# external iteration with .next:
e = Countdown.new(3).each
puts e.next
puts e.next3️⃣ Lazy Enumerators, each_with_object & with_index
Chaining map and select on an infinite range would hang forever — unless you call .lazy first, which pulls values one at a time only as far as you actually consume. Meanwhile each_with_object threads a result object through the loop, and with_index numbers items from any starting point.
# Lazy enumerators process huge (even infinite) sequences without building arrays.
naturals = (1..Float::INFINITY).lazy # infinite, but lazy = safe
first_five_even_squares = naturals
.map { |n| n * n } # 1, 4, 9, 16, 25, ...
.select { |n| n.even? } # 4, 16, 36, 64, ...
.first(5)
p first_five_even_squares
# each_with_object builds up a result object while iterating.
words = %w[apple banana apple cherry banana apple]
counts = words.each_with_object(Hash.new(0)) do |word, tally|
tally[word] += 1
end
p counts
# with_index lets you number items starting anywhere.
%w[gold silver bronze].each.with_index(1) do |medal, place|
puts "#{place}. #{medal}"
end🎯 Your Turn
One keyword is missing. Replace the ___ so each item is handed to the block, making the whole Enumerable toolkit light up.
# 🎯 YOUR TURN — fill in the ___ so the class is fully iterable.
class Inventory
include Enumerable
def initialize(items)
@items = items
end
def each
@items.each { |item| ___ item } # 👉 which keyword hands each item to the block?
end
end
stock = Inventory.new(["sword", "shield", "potion", "map"])
# These should all work because Enumerable is included:
puts stock.map { |i| i.capitalize }.inspect
puts stock.sort.inspect
puts stock.min_by { |i| i.length }
# ✅ Expected output:
# ["Sword", "Shield", "Potion", "Map"]
# ["map", "potion", "shield", "sword"]
# mapCommon Errors (and the fix)
- "undefined method 'map'" — ❌ You forgot include Enumerable. ✅ Add it inside the class; each alone is not enough.
- "undefined method 'each' for ..." — ❌ Enumerable needs an each to delegate to. ✅ Define def each; ...; yield x; end.
- Program hangs on an infinite range — ❌ (1..Float::INFINITY).map { ... } never finishes. ✅ Insert .lazy before the chain.
- each returns nil instead of chaining — ❌ No guard for a missing block. ✅ Start with return to_enum(:each) unless block_given?.
- "StopIteration" from .next — ❌ You called .next past the last element. ✅ Wrap in rescue StopIteration or use peek.
Pro Tips
- 💡 One method, fifty gifts: define each well and Enumerable does the rest — don't reimplement map by hand.
- 💡 Custom sort: Enumerable's sort/min need elements that compare; the next lessons cover keyword args and Comparable.
- 💡 Enumerator.new: for sequences with no backing array (like Fibonacci), build one directly with a yielder.
Mini-Challenge: A Fibonacci Enumerator
Build an infinite Fibonacci sequence with Enumerator.new, then use first and lazy to pull slices out of it. Run with ruby fib.rb.
# 🎯 MINI-CHALLENGE: build a Fibonacci Enumerator.
# Save as fib.rb and run with: ruby fib.rb
#
# 1. Use Enumerator.new { |yielder| ... } and an infinite loop.
# 2. Push each Fibonacci number with yielder << value.
# 3. Take the first 10, then use .lazy to find the first 5 over 100.
#
# ✅ Expected output:
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# [144, 233, 377, 610, 987]
# your code here📋 Quick Reference — Iterators & Enumerator
| Goal | Ruby |
|---|---|
| Make a class iterable | include Enumerable + def each |
| Return an Enumerator | return to_enum(:each) unless block_given? |
| Lazy infinite chain | range.lazy.map.select.first(n) |
| Build a result | each_with_object(Hash.new(0)) |
| Number items | each.with_index(1) |
| Sequence with no array | Enumerator.new { |y| y << x } |
🎉 Lesson Complete!
- ✅ Define each + include Enumerable for free collection methods
- ✅ Return to_enum(:each) so a block-less call gives an Enumerator
- ✅ .lazy makes infinite sequences safe to chain
- ✅ each_with_object builds up a result while iterating
- ✅ with_index numbers items from any start value
Practice quiz
Which single method must you define to make a class iterable?
- map
- next
- each
- iterate
Answer: each. Define each (which yields elements) and the rest can build on it.
What does including Enumerable give a class that defines each?
- map, select, count and dozens more
- nothing extra
- only count
- automatic threading
Answer: map, select, count and dozens more. Enumerable supplies 50+ collection methods built on top of each.
Inside each, which keyword hands an element to the caller's block?
- return
- give
- pass
- yield
Answer: yield. yield passes each value to the block supplied at the call site.
When you call each with NO block, what should an idiomatic method return?
- nil
- an Enumerator
- an empty array
- false
Answer: an Enumerator. Returning an Enumerator (via to_enum) makes the method chainable.
What is the class of [1, 2, 3].each when called without a block?
- Enumerator
- Array
- NilClass
- Proc
Answer: Enumerator. A block-less each returns an Enumerator object.
Why call .lazy before chaining map/select on (1..Float::INFINITY)?
- to sort it
- to make it faster to print
- so values are pulled on demand instead of building an infinite array
- it is required syntax
Answer: so values are pulled on demand instead of building an infinite array. Lazy enumerators evaluate one element at a time, only as far as consumed.
What does [1,2,3].each_with_object(Hash.new(0)) do?
- returns a sorted array
- threads one mutable object through the loop and returns it
- raises an error
- creates three hashes
Answer: threads one mutable object through the loop and returns it. each_with_object carries a result object through iteration and returns it.
What does %w[gold silver].each.with_index(1) start counting from?
- 0
- the array length
- -1
- 1
Answer: 1. with_index(1) numbers items starting at 1 instead of 0.
How do you pull the next value from an Enumerator one at a time?
- .pop
- .next
- .shift
- .take
Answer: .next. External iteration uses .next, which remembers where it left off.
Calling .next past the last element of an Enumerator raises:
- NoMethodError
- ArgumentError
- StopIteration
- nothing
Answer: StopIteration. Exhausting an Enumerator with .next raises StopIteration.
Continue this course
- Previous: Checkpoint: OOP & Stdlib
- Next: Method Arguments (Keyword, Splat, Block) — Positional, keyword, *args, **kwargs, and &block arguments
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is the difference between an iterator and an Enumerator in Ruby?
An iterator is any method that yields values to a block — each, map, and times are all iterators. An Enumerator is a concrete object that captures iteration so you can pause it, chain it, or iterate externally with .next. When you call an iterator method like each without a block, well-behaved Ruby methods return an Enumerator instead, which is why array.each.with_index works. Enumerators turn 'a loop happening now' into 'a value you can hold and manipulate later'.
Why include Enumerable instead of writing map and select myself?
Enumerable is a mixin that implements over 50 collection methods — map, select, reject, reduce, sort, min_by, group_by, and many more — entirely in terms of one method you provide: each. Include the module and define each, and your custom class instantly behaves like an Array or Hash for traversal. It is the single biggest payoff-for-effort feature in Ruby's object model: one method written, dozens gained.
What makes a lazy enumerator different from a normal one?
A normal chain like (1..Infinity).map { ... }.first(5) would try to build the entire mapped array first and hang forever. Calling .lazy makes each step pull values one at a time, on demand, so map and select only run as far as first(5) actually needs. Lazy enumerators let you express infinite or very large sequences declaratively and only compute the slice you consume. They trade a little per-element overhead for the ability to work with streams that would never fit in memory.
When should I return to_enum(:each) from my each method?
Return to_enum(:each) (or enum_for(:each)) at the top of each, guarded by unless block_given?, so that calling each with no block hands back an Enumerator rather than doing nothing or raising. This matches how Ruby's built-in collections behave and makes your class chainable: callers can write your_object.each.with_index, your_object.each.lazy, or your_object.each.next. It is the idiomatic way to make a custom iterator a first-class, composable Enumerator.