Custom Iterators & Enumerator
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.
Learn Custom Iterators & Enumerator in our free Ruby course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Ruby course at LearnCodingFast — hands-on lessons with examples you run in your browser, 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
1️⃣ each + Enumerable = Free Superpowers
The deal Ruby offers is generous: write a single each method that yield s 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 .
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.
3️⃣ 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.
🎯 Your Turn
One keyword is missing. Replace the ___ so each item is handed to the block, making the whole Enumerable toolkit light up.
Common 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 .
📋 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 }
Frequently Asked Questions
🎉 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
- ✅ Next lesson: Method Arguments — keyword, splat, and block parameters
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