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

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.next

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.

# 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"]
#    map

Common Errors (and the fix)

Pro Tips

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

GoalRuby
Make a class iterableinclude Enumerable + def each
Return an Enumeratorreturn to_enum(:each) unless block_given?
Lazy infinite chainrange.lazy.map.select.first(n)
Build a resulteach_with_object(Hash.new(0))
Number itemseach.with_index(1)
Sequence with no arrayEnumerator.new { |y| y << x }

🎉 Lesson Complete!

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

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.