How to use an enumerator

Learn how to use an enumerator with practical examples, diagrams, and best practices. Covers ruby, enumeration, enumerator development techniques with visual explanations.

Mastering Enumerators in Ruby: A Comprehensive Guide

Hero image for How to use an enumerator

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.

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.