How to use an enumerator
Categories:
Mastering Enumerators in Ruby: A Comprehensive Guide

Unlock the power of Ruby's Enumerator class to create flexible, lazy, and reusable iteration patterns for any collection or custom data source.
Ruby's Enumerable
module provides a rich set of methods for iterating over collections. However, sometimes you need more control over the iteration process, or you want to create an enumerator from a non-collection source. This is where the Enumerator
class comes into play. An Enumerator
object represents an external iterator, allowing you to explicitly control when and how elements are yielded. This article will guide you through the fundamentals of Enumerator
, its practical applications, and how to leverage it for powerful, lazy evaluation.
What is an Enumerator?
At its core, an Enumerator
is an object that encapsulates an iteration process. Instead of iterating directly, you can obtain an Enumerator
object and then manually advance it using methods like next
, rewind
, and peek
. Many Enumerable
methods, when called without a block, return an Enumerator
themselves, allowing for method chaining and lazy evaluation. This makes Enumerator
a powerful tool for building flexible and efficient data processing pipelines.
# Calling an Enumerable method without a block returns an Enumerator
a = [1, 2, 3, 4, 5].map
puts a.class # => Enumerator
# Manually advancing the enumerator
puts a.next # => 1
puts a.next # => 2
# Creating an enumerator from scratch using Enumerator.new
b = Enumerator.new do |yielder|
i = 0
loop do
yielder << i
i += 1
end
end
puts b.next # => 0
puts b.next # => 1
puts b.next # => 2
Basic examples of obtaining and creating Enumerator objects.
Key Methods and Their Usage
Understanding the core methods of the Enumerator
class is crucial for effective use. These methods allow you to control the flow of iteration, inspect upcoming elements, and reset the enumerator's state. The next
method is the most fundamental, advancing the enumerator and returning the next element. rewind
resets the enumerator to its initial state, allowing you to iterate from the beginning again. peek
allows you to look at the next element without consuming it, which can be useful for conditional logic within an iteration.
flowchart TD A[Start Iteration] --> B{Call `next`?} B -- Yes --> C[Yield Element] C --> D{More Elements?} D -- Yes --> B D -- No --> E[Stop Iteration] B -- No --> F{Call `rewind`?} F -- Yes --> A F -- No --> G{Call `peek`?} G -- Yes --> H[Return Next Element (Don't Consume)] H --> B G -- No --> E
Flowchart illustrating the interaction of next
, rewind
, and peek
methods.
enum = [10, 20, 30].each
puts "First pass:"
puts enum.next # => 10
puts enum.next # => 20
enum.rewind # Reset the enumerator
puts "Second pass after rewind:"
puts enum.next # => 10
puts "Peeking at the next element: #{enum.peek}" # => 20
puts "Calling next after peek: #{enum.next}" # => 20 (peek didn't consume it)
begin
enum.next # => 30
enum.next # This will raise StopIteration
rescue StopIteration
puts "End of iteration reached!"
end
Demonstrating next
, rewind
, and peek
with error handling.
Enumerator.new
, remember to use yielder << value
to yield elements. The block passed to Enumerator.new
is executed lazily, only when next
is called.Practical Applications: Lazy Evaluation and Infinite Sequences
One of the most powerful features of Enumerator
is its ability to facilitate lazy evaluation. This means that elements are generated or processed only when they are explicitly requested, rather than all at once. This is incredibly useful for working with large datasets, infinite sequences, or computationally expensive operations, as it conserves memory and processing power. You can create enumerators for infinite sequences, such as Fibonacci numbers or prime numbers, without ever generating the entire sequence in memory.
# An infinite sequence of Fibonacci numbers
fib = Enumerator.new do |yielder|
a, b = 0, 1
loop do
yielder << a
a, b = b, a + b
end
end
puts "First 5 Fibonacci numbers:"
5.times { puts fib.next }
# Using take to get a finite portion of an infinite sequence
puts "\nNext 3 Fibonacci numbers using take:"
puts fib.take(3).inspect # => [8, 13, 21]
# Chaining enumerators for lazy processing
long_list = (1..Float::INFINITY).lazy.map { |x| x * 2 }.select { |x| x % 3 == 0 }
puts "\nFirst 5 multiples of 6:"
long_list.first(5).each { |x| puts x }
Examples of infinite sequences and lazy evaluation with Enumerator.
.lazy
method, available on Enumerable
objects, returns an Enumerator::Lazy
object. This allows you to chain multiple Enumerable
methods (like map
, select
, reject
) without creating intermediate arrays, leading to significant performance improvements for large collections.