Enumerators and Lazy Evaluation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. An enumerator is an object that stands for an iteration which has not happened yet: it remembers a receiver, a method and its arguments, and it runs that method only when something asks for the elements. That small idea explains why [1, 2, 3].each without a block does not fail but returns an Enumerator, why map.with_index(1) works, how loop and next can walk a collection one element at a time, and how an infinite sequence such as the Fibonacci numbers can be an ordinary object that you pass around and take ten elements from. The page covers where enumerators come from (methods called without a block, to_enum, Enumerator.new, Enumerator.produce, +, Enumerator.product and step), the two ways to consume them (internal iteration with a block and external iteration with next), the lazy variant Enumerator::Lazy that makes a whole pipeline of map, select and take process one element at a time, the three specialised classes Enumerator::Chain, Enumerator::Product and Enumerator::ArithmeticSequence, and the errors and pitfalls that surprise newcomers.

Related pages: every method an enumerator offers beyond its own comes from Enumerable, described in Enumerable, and the blocks that drive it are explained in Blocks, Procs and Lambdas. Infinite and stepped ranges are covered in Ranges, and slicing arrays with a stepped range in Arrays. External iteration is built on fibers, the subject of Fibers and the Fiber Scheduler, and its thread restriction is a consequence of the model in Threads and the GVL. How StopIteration is raised and rescued is a case of Exceptions and Error Handling, and the anonymous block parameter used in some examples is explained in Methods and Parameters. For the same idea in other languages, see Rust iterators (where every adaptor is lazy by default), Python iterators and generators, PHP iterators and generators and Java streams.

Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 without warnings, and linted with RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings, NewCops: enable, Style/Documentation off). The output shown in # => comments is the real output of that run. Results that depend on a clock, an object address or a random number are avoided on purpose.

What an enumerator is

Internal and external iteration

There are two ways to walk a sequence. With internal iteration the sequence drives: you hand it a block and it calls the block once per element (each, map, select). With external iteration you drive: you ask for the next element when you are ready (next), keep the position yourself and may do other things in between. Most languages offer one of the two natively and simulate the other; Ruby’s Enumerator offers both on the same object, so the same titles.each can be passed a block or be advanced by hand.

An enumerator is, in essence, a triple: a receiver, the name of an iteration method and the arguments for it. [1, 2, 3].each_slice(2) is the object [1, 2, 3], the method each_slice and the argument 2; calling each on the enumerator with a block calls [1, 2, 3].each_slice(2) { ... }. Nothing is copied and nothing runs until the enumerator is consumed. Enumerator includes Enumerable, so it answers map, select, first, take, zip, each_slice and everything else described in Enumerable.

The family

Enumerator is the base class and several subclasses add behaviour. They are normally created by a method rather than with new, which is why their names are easy to miss in code.

Class Created by What it is for

Enumerator

A method without a block (each, map, each_slice), to_enum, enum_for, Enumerator.new, Enumerator.produce

The general case: internal and external iteration, size, with_index, with_object, rewind.

Enumerator::Lazy

Enumerable#lazy, Enumerator::Lazy.new

Stages (map, select, take, …​) that run one element at a time and stop as early as possible.

Enumerator::Chain

Enumerator, the plus method, Enumerable#chain, Enumerator::Chain.new

Several enumerables treated as one, iterated one after the other (Ruby 2.6).

Enumerator::Product

Enumerator.product, Enumerator::Product.new

The Cartesian product of several enumerables, computed on demand (Ruby 3.2).

Enumerator::ArithmeticSequence

Numeric#step, Range#step, Range#%

A number sequence with a fixed difference that knows its size and last without iterating (Ruby 2.6).

Enumerator::Yielder

The block parameter of Enumerator.new

The object whose << and yield produce the elements; not created by user code.

Enumerator::Generator, Enumerator::Producer

Enumerator.new, Enumerator.produce

Internal receivers that run your block. They show up in inspect output and are not used directly.

classDiagram class Enumerable { <<module>> } class Lazy["Enumerator::Lazy"] class Chain["Enumerator::Chain"] class Product["Enumerator::Product"] class ArithmeticSequence["Enumerator::ArithmeticSequence"] class Generator["Enumerator::Generator"] class Yielder["Enumerator::Yielder"] Enumerable <|.. Enumerator : includes Enumerator <|-- Lazy Enumerator <|-- Chain Enumerator <|-- Product Enumerator <|-- ArithmeticSequence Enumerator ..> Generator : Enumerator.new wraps the block Generator ..> Yielder : passes it to the block

Two of the subclasses are not full cursors. Enumerator::Chain and Enumerator::Product undefine next, peek, next_values, peek_values and feed, so chain.next raises NoMethodError; the section on chains shows the one-line workaround. Enumerator::ArithmeticSequence keeps them.

Methods without a block return enumerators

The convention

Almost every iteration method in the core library has two forms: with a block it iterates and returns its normal result, without a block it returns an enumerator for the same call. [1, 2, 3].map is not an error; it is the object #<Enumerator: [1, 2, 3]:map>, and its inspect shows the receiver, a colon, the method and the arguments. The list includes each, map, select, reject, each_with_index, each_slice, each_cons, each_char, each_line, Integer#times, upto, downto, Hash#each, Kernel#loop, IO.foreach and many more. The convention is what lets methods be chained: the enumerator is the first link, with_index or with_object the second, and the original method’s block the last.

Enumerator#size answers without iterating when the receiver can compute it (an Array, a Range, times, each_slice on an Array) and returns nil when it cannot (a custom each, a select). loop answers Infinity. A nil size does not mean empty; it means "unknown without walking it".

# frozen_string_literal: true

require "shelf"

catalog = Shelf::Catalog.new
[["9780441013593", "Dune", "Frank Herbert"], ["9780261103573", "The Hobbit", "J. R. R. Tolkien"],
 ["9780553293357", "Foundation", "Isaac Asimov"]].each do |digits, title, name|
  catalog.add(Shelf::Book.new(Shelf::Isbn.new(digits), title, Shelf::Author.new(name)))
end

titles = catalog.map(&:title)
p titles.each # => #<Enumerator: ["Dune", "The Hobbit", "Foundation"]:each>
p titles.each.class # => Enumerator
p titles.map # => #<Enumerator: ["Dune", "The Hobbit", "Foundation"]:map>
p titles.each_slice(2) # => #<Enumerator: ["Dune", "The Hobbit", "Foundation"]:each_slice(2)>
p titles.each_with_index # => #<Enumerator: ["Dune", "The Hobbit", "Foundation"]:each_with_index>
p 3.times # => #<Enumerator: 3:times>
p "shelf".each_char # => #<Enumerator: "shelf":each_char>
p((1..4).each_cons(2)) # => #<Enumerator: 1..4:each_cons(2)>
p catalog.each.class # => Enumerator

# An enumerator knows its size when the receiver can compute it without iterating.
p titles.each.size # => 3
p titles.each_slice(2).size # => 2
p 3.times.size # => 3
p catalog.each.size # => nil
p loop.size # => Infinity

# Chaining a method on the enumerator is how Ruby spells "map with an index".
p titles.each.with_index(1).to_a # => [["Dune", 1], ["The Hobbit", 2], ["Foundation", 3]]
p(titles.map.with_index(1) { |title, number| "#{number}. #{title}" }) # => ["1. Dune", "2. The Hobbit", "3. Foundation"]
p(titles.select.with_index { |_title, index| index.even? }) # => ["Dune", "Foundation"]
p(titles.each_with_index.map { |title, index| [index, title.length] }) # => [[0, 4], [1, 10], [2, 10]]
p titles.each.with_object([]).class # => Enumerator
p(titles.each_with_object(Hash.new(0)) { |title, memo| memo[title.length] += 1 }) # => {4 => 1, 10 => 2}

Source: Enumerator (the class overview, size, with_index and with_object), Kernel#loop and Enumerable.

The Catalog#each of the shelf gem follows the convention (return enum_for(:each) unless block_given?), which is why catalog.each.class is Enumerator and catalog.map(&:title) works; its size is nil because the method gives no way to compute it.

with_index, each_with_index and with_object

Three methods attach extra information to an enumeration.

Method Behaviour

with_index(offset = 0)

Yields [element, index], counting from offset (any value that converts to an Integer, and nil means 0). With a block it returns the result of the underlying method, so map.with_index returns an Array and select.with_index a filtered one; without a block it returns another enumerator.

each_with_index

The same with the offset fixed at 0. It is defined by Enumerable; with_index is defined by Enumerator and is what you chain after another enumerator.

with_object(memo), each_with_object(memo)

Yields [element, memo] with the same object every time and returns the memo, not the result of the underlying method. Meant for accumulating into a Hash, Array or Set.

Block parameters destructure: hash.map.with_index(1) { |(key, value), index| ... } needs the parentheses around the pair, because the enumerator yields [[key, value], index]. RuboCop reports each_with_object calls that only build a Hash as Style/ReduceToHash and suggests to_h { ... }, which is usually clearer.

to_enum and enum_for

to_enum and enum_for (aliases, defined in Kernel) create an enumerator for any method of any object: obj.to_enum(:method, *args). Calling each on it later calls obj.method(*args) with a block. They are how your own iteration methods follow the convention above, with the guard return to_enum(:name) unless block_given?. Three details matter.

  • Pass the method name with __method__ (the name of the current method) so that renaming the method does not break the guard, and pass every argument the method takes after it; the enumerator replays them.

  • A block given to to_enum computes the size lazily, in the context of the receiver. Give one only when the size is cheap and certain; otherwise size stays nil, which is honest.

  • The guard must be the first statement. A method that does work before it would repeat that work every time the enumerator is iterated.

# frozen_string_literal: true

module Shelf
  # A shelf of books that exposes its contents as enumerators.
  class Rack
    def initialize(*titles) = @titles = titles

    def inspect = "#<Shelf::Rack #{@titles.size} titles>"

    # The classic guard: no block, return an enumerator that calls this method again.
    def each_title(&)
      return to_enum(:each_title) { @titles.size } unless block_given?

      @titles.each(&)
      self
    end

    # __method__ avoids repeating the method name; extra arguments are replayed to the method.
    def each_pair_of(size, &)
      return enum_for(__method__, size) { @titles.size.fdiv(size).ceil } unless block_given?

      @titles.each_slice(size, &)
    end

    # A method that cannot compute its size cheaply simply omits the size block.
    def each_long_title(min)
      return to_enum(__method__, min) unless block_given?

      @titles.each { |title| yield title if title.length >= min }
    end
  end
end

rack = Shelf::Rack.new("Dune", "The Hobbit", "Foundation", "Emma", "Ulysses")
p rack.each_title.size # => 5
p rack.each_title.to_a # => ["Dune", "The Hobbit", "Foundation", "Emma", "Ulysses"]
p rack.each_title # => #<Enumerator: #<Shelf::Rack 5 titles>:each_title>
p rack.each_pair_of(2) # => #<Enumerator: #<Shelf::Rack 5 titles>:each_pair_of(2)>
p rack.each_pair_of(2).size # => 3
p rack.each_pair_of(2).to_a # => [["Dune", "The Hobbit"], ["Foundation", "Emma"], ["Ulysses"]]
p rack.each_long_title(8).size # => nil
p rack.each_long_title(8).to_a # => ["The Hobbit", "Foundation"]
p(rack.each_title.with_index(1).map { |title, number| "#{number}:#{title}" }.first(2)) # => ["1:Dune", "2:The Hobbit"]
p(rack.each_title { nil }.class) # => Shelf::Rack

# An enumerator is lazy about the call itself: nothing runs until something iterates it.
enum = rack.each_title
p enum.next # => "Dune"
p enum.take(2) # => ["Dune", "The Hobbit"]

A class that defines each this way and includes Enumerable gets the whole Enumerable API for free (see Enumerable). each_title above also shows two forms of block forwarding. def each_title(&) followed by @titles.each(&) forwards the caller’s block without naming it (anonymous block parameter, Ruby 3.1), and each_long_title calls yield directly because it decides per element. RuboCop’s Style/ExplicitBlockArgument pushes you to the first form whenever a method only wraps a block in another block.

Enumerator#each with a block

Calling each on an enumerator and passing a block runs the underlying method with that block, and returns what the method returns. %w[a b c].map.each(&:upcase) is %w[a b c].map(&:upcase), and select.each { ... } filters. This is the mechanism behind map.with_index and select.with_index: the block of with_index is given to the original method, with the index added to its arguments. It also means an enumerator for a mutating or side-effecting method repeats the side effect each time it is iterated.

External iteration

next, peek, rewind and StopIteration

next returns the next element and advances; peek returns it without advancing; rewind starts again. When the elements run out next and peek raise StopIteration (message iteration reached an end), and keep raising it until rewind. The exception carries the return value of the iteration in result, which is the value the underlying method returned (for each on an Array, the array itself).

Method Behaviour

next

Returns the next element and moves on. Raises StopIteration at the end.

peek

Returns the next element without moving. Raises StopIteration at the end.

next_values, peek_values

Like next and peek, but always return an Array of the values yielded: yield 1 gives [1], yield 2, 3 gives [2, 3], yield [4, 5] gives [[4, 5]], a bare yield gives [] and yield nil gives [nil]. next returns the same thing in the most convenient shape (the single value, an Array for several, nil for none).

rewind

Resets the position; also calls rewind on the receiver if it defines one. Returns the enumerator.

feed(value)

Sets the value the next yield inside the producer will return (see below). Raises TypeError: feed value already set when called twice before the next next.

size

The number of elements, or nil/Infinity.

Kernel#loop rescues StopIteration and returns its result, so the idiomatic way to drain a cursor is loop { ... cursor.next ... }. Written with a plain while true the exception has to be rescued by hand. The exception is also why a StopIteration that escapes from the producer (the block of Enumerator.new) is dangerous: to_a does not swallow it, loop does, and the two can silently disagree (see the pitfalls section).

# frozen_string_literal: true

def outcome
  yield.inspect
rescue StandardError => e
  "#{e.class}: #{e.message}"
end

titles = %w[Dune Emma Ulysses]
cursor = titles.each

p cursor.peek # => "Dune"
p cursor.next # => "Dune"
p cursor.next # => "Emma"
p cursor.peek # => "Ulysses"
p cursor.next # => "Ulysses"
puts(outcome { cursor.next }) # => StopIteration: iteration reached an end
puts(outcome { cursor.peek }) # => StopIteration: iteration reached an end

# rewind starts again from the first element.
cursor.rewind
p cursor.next # => "Dune"

# The exception carries the return value of the iteration in #result.
cursor = titles.each
3.times { cursor.next }
begin
  cursor.next
rescue StopIteration => e
  p e.result # => ["Dune", "Emma", "Ulysses"]
  p e.message # => "iteration reached an end"
end

# Kernel#loop rescues StopIteration and returns the result: the idiomatic way to drain a cursor.
cursor = titles.each_with_index
seen = []
result = loop do
  title, index = cursor.next
  seen << "#{index}:#{title}"
end
p seen # => ["0:Dune", "1:Emma", "2:Ulysses"]
p result # => ["Dune", "Emma", "Ulysses"]

# next_values and peek_values keep the difference between yield 1, yield [1] and a bare yield.
generator = Enumerator.new do |y|
  y << 1
  y.yield 2, 3
  y.yield [4, 5]
  y.yield
  y.yield nil
end
values = []
loop { values << generator.next_values }
p values # => [[1], [2, 3], [[4, 5]], [], [nil]]
generator.rewind
plain = []
loop { plain << generator.next }
p plain # => [1, [2, 3], [4, 5], nil, nil]
p generator.rewind.peek_values # => [1]

When you need a cursor

Internal iteration is simpler and faster, and should be the default. External iteration earns its place when two or more sequences must advance together under your control, which a single block cannot express. The classic case is merging two sorted sources: each source is a cursor, peek compares the heads and next consumes the smaller one. The same shape serves a lexer or parser reading tokens with one-token look-ahead, round-robin scheduling and any "pull" protocol. Note that the merge below is itself an enumerator, so it is lazy and can be taken from with first(3).

# frozen_string_literal: true

# A merge of two sorted sources needs both cursors alive at once: external iteration.
def merge_sorted(left, right)
  Enumerator.new do |y|
    a = left.each
    b = right.each
    # loop ends quietly when a.peek or b.peek raises StopIteration.
    loop { y << (a.peek <= b.peek ? a.next : b.next) }
    # Only one side can have elements left; drain both.
    loop { y << a.next }
    loop { y << b.next }
  end
end

p merge_sorted([1, 4, 9], [2, 3, 10, 11]).to_a # => [1, 2, 3, 4, 9, 10, 11]
p merge_sorted(%w[Dune Ulysses], %w[Emma Hobbit Zorba]).first(3) # => ["Dune", "Emma", "Hobbit"]

# Internal iteration cannot do this: zip over two infinite sources needs the cursor protocol.
evens = Enumerator.produce(0) { it + 2 }
odds = Enumerator.produce(1) { it + 2 }
pairs = Enumerator.new { |y| loop { y << [evens.next, odds.next] } }
p pairs.take(3) # => [[0, 1], [2, 3], [4, 5]]
# The cursors live outside the enumerator, so the second call continues where the first stopped.
p pairs.take(3) # => [[6, 7], [8, 9], [10, 11]]

# Each external enumerator holds a suspended Fiber, so a cursor must not cross threads.
Thread.report_on_exception = false
cursor = [1, 2, 3].each
cursor.next
begin
  Thread.new { cursor.next }.join
rescue FiberError => e
  puts "#{e.class}: #{e.message}" # => FiberError: fiber called across threads
end

The second part of the example shows a trap: the two cursors evens and odds live outside the generator, so they keep their position between calls, and the second take(3) continues where the first one stopped. State held outside a generator is shared state.

feed

feed(value) is the one way to talk back to a producer. Inside a producer, y.yield(x) (or the block call of map) evaluates to the value that was fed; without a feed it evaluates to nil. For map that value becomes the mapped element, so feeding every step reproduces map by hand. It is rare in application code, but it shows that an external enumerator is a coroutine, and it is how an external iterator can be turned back into an internal one (the class documentation contains that example).

What next costs, and what it forbids

next, next_values, peek and peek_values are the only enumerator methods that use external iteration (the other one in core is Array#zip with a non-Array argument). To suspend the producer in the middle of its loop, Ruby runs it in a fiber, a lightweight coroutine; that has consequences that internal iteration does not have.

  • The fiber adds overhead compared with internal iteration, so do not use next in a hot loop over a collection you could simply each.

  • The enumerator belongs to the fiber that created it. Calling next from another thread raises FiberError: fiber called across threads; an enumerator cursor is not something to share between threads (see Threads and the GVL). Give each thread its own enumerator.

  • A frozen enumerator cannot advance: next, peek, rewind and feed raise FrozenError.

  • The stack trace of an exception raised in the producer shows only the producer’s frames, not the caller’s.

  • Fiber-local variables (Thread.current[:name]) are not inherited by the producer’s fiber, while fiber storage (Fiber[:name], Ruby 3.2) is inherited as a copy: reads work, writes stay in the producer’s fiber. Internal iteration runs in the caller’s fiber and sees and changes both.

# frozen_string_literal: true

Thread.current[:locale] = "en"
Fiber[:request_id] = "r-17"

report = Enumerator.new do |y|
  y << [Thread.current[:locale], Fiber[:request_id]]
  Fiber[:request_id] = "changed"
end

# Internal iteration runs in the caller's fiber: it sees fiber-local variables and its writes are visible.
p report.first # => ["en", "r-17"]
p report.to_a # => [["en", "r-17"]]
p Fiber[:request_id] # => "changed"

# External iteration runs the block in a new fiber: fiber-local variables are gone, fiber storage is inherited.
Fiber[:request_id] = "r-17"
cursor = report.each
p cursor.next # => [nil, "r-17"]
begin
  cursor.next
rescue StopIteration
  p Fiber[:request_id] # => "r-17"
end

Source: Enumerator (the section "External Iteration"), Fiber.[] and FiberError.

Enumerator.new and the yielder

Enumerator.new(size = nil) { |y| ... } builds an enumerator from a block. The block runs when the enumerator is iterated and receives an Enumerator::Yielder; every y << value or y.yield value hands one element to the consumer and suspends the block until the consumer wants more. This is the Ruby spelling of a generator.

  • y << value returns the yielder, so it chains: y << 1 << 2. It yields one value.

  • y.yield a, b can pass several values (the consumer sees [a, b]) and returns the value fed with feed, or nil.

  • y.to_proc makes the yielder usable as a block: other.each(&y) forwards every element of other (Ruby 2.7).

  • The optional size argument becomes the result of Enumerator#size. It can be an Integer, Float::INFINITY or a callable that is evaluated on each call to size; without it the size is nil.

  • Calling Enumerator.new without a block raises ArgumentError, and so does the two-argument form Enumerator.new(object, :method) that very old code used; write object.to_enum(:method) instead.

The block is the recipe, and it starts from the top for every iteration. Two external consequences follow. First, a generator that logs or has side effects repeats them for each consumer, and a consumer that stops early (first(1)) stops the block at the y << it was waiting on, so the code after it never runs. Second, variables created inside the block start fresh each time, while variables captured from outside are shared by all iterations (the leaky example). Keep the state of a generator inside the block unless sharing is the point.

# frozen_string_literal: true

require "shelf"

# << returns the yielder (so it chains); yield passes several values.
log = []
numbers = Enumerator.new do |y|
  log << :start
  y << 1 << 2
  y.yield 3, 4
  log << :end
  :finished
end
p numbers.to_a # => [1, 2, [3, 4]]
p log # => [:start, :end]
p numbers.size # => nil
p(numbers.each { nil }) # => :finished

# The block runs from the top for every iteration, and only as far as the consumer asks.
log.clear
p numbers.first(1) # => [1]
p log # => [:start]
p numbers.next # => 1
p log # => [:start, :start]

# State kept outside the block is shared by every iteration; state created inside it starts fresh.
counter = 0
leaky = Enumerator.new { |y| 3.times { y << (counter += 1) } }
p leaky.to_a # => [1, 2, 3]
p leaky.to_a # => [4, 5, 6]

# Infinite generators are fine because the consumer decides when to stop.
fibonacci = Enumerator.new do |y|
  a = 0
  b = 1
  loop do
    y << a
    a, b = b, a + b
  end
end
p fibonacci.take(10) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
p fibonacci.lazy.select(&:even?).first(5) # => [0, 2, 8, 34, 144]
p(fibonacci.each_with_index.find { |n, _| n > 100 }) # => [144, 12]
p fibonacci.size # => nil

# The optional argument is the size: a number, Float::INFINITY, or a callable evaluated on demand.
sized = Enumerator.new(3) { |y| 3.times { y << it } }
p sized.size # => 3
p(Enumerator.new(Float::INFINITY) { |y| loop { y << 1 } }.size) # => Infinity
calls = 0
dynamic = Enumerator.new(-> { calls += 1 }) { |y| y << 1 }
p [dynamic.size, dynamic.size] # => [1, 2]

# A Yielder can be passed where a block is expected: y.to_proc is what &y calls.
catalog = Shelf::Catalog.new
catalog.add(Shelf::Book.new(Shelf::Isbn.new("9780441013593"), "Dune", Shelf::Author.new("Frank Herbert")))
titles = Enumerator.new { |y| catalog.each { |book| y << book.title } }
direct = Enumerator.new { |y| catalog.map(&:title).each(&y) }
p titles.to_a # => ["Dune"]
p direct.to_a # => ["Dune"]

# Generators are values: pass them to methods that expect any Enumerable.
def numbered(source) = source.each_with_index.map { |item, index| "#{index + 1}. #{item}" }
p numbered(titles) # => ["1. Dune"]

Enumerator.new is the right tool when the sequence is naturally written as a loop with some local state: Fibonacci numbers, a paginated API that you want to hide behind a flat stream, a tree traversal, a file read in chunks. When each element is a function of the previous one, Enumerator.produce below says the same thing with less code.

Enumerator.produce

Enumerator.produce(initial = nil) { |previous| next_value } (Ruby 2.7) builds an infinite enumerator in which each element is the block applied to the previous one. With an initial value the first element is that value; without one the block is called with no argument to get the first. The sequence ends when the block raises StopIteration, which is the only way to make it finite. It is the functional "unfold": a seed and a step function.

Ruby 4.0 adds the size: keyword. The default is Float::INFINITY (which is wrong for a sequence that stops by StopIteration, as the Collatz example shows), nil means unknown, an Integer states the size, and a callable computes it when size is called. On Ruby 3.4 the keyword does not exist and the call raises ArgumentError.

# frozen_string_literal: true

def outcome
  yield.inspect
rescue StandardError => e
  "#{e.class}: #{e.message}"
end

# produce(initial) yields initial, then block(initial), block(block(initial)), and so on.
p(Enumerator.produce(1) { it * 2 }.take(6)) # => [1, 2, 4, 8, 16, 32]
p Enumerator.produce(1, &:succ).lazy.select(&:even?).first(3) # => [2, 4, 6]

# Without an initial value the first element is the first call of the block (called with no argument).
counter = 0
ticks = Enumerator.produce { counter += 1 }
p ticks.take(3) # => [1, 2, 3]

# Raising StopIteration inside the block ends the sequence: this is how finite sequences are made.
countdown = Enumerator.produce(3) { it.zero? ? raise(StopIteration) : it - 1 }
p countdown.to_a # => [3, 2, 1, 0]

# The Collatz sequence: each element depends on the previous one, which is what produce is for.
collatz = Enumerator.produce(6) do |n|
  raise StopIteration if n == 1

  n.even? ? n / 2 : (3 * n) + 1
end
p collatz.to_a # => [6, 3, 10, 5, 16, 8, 4, 2, 1]
p collatz.size # => Infinity

# Due dates of a loan that is renewed every 14 days, as Time values.
lent_on = Time.utc(2026, 10, 1)
due_dates = Enumerator.produce(lent_on + (14 * 86_400)) { it + (14 * 86_400) }
p(due_dates.first(3).map { it.strftime("%Y-%m-%d") }) # => ["2026-10-15", "2026-10-29", "2026-11-12"]
labels = due_dates.lazy.map { it.strftime("%b %-d") }
p(labels.take_while { it != "Dec 24" }.to_a) # => ["Oct 15", "Oct 29", "Nov 12", "Nov 26", "Dec 10"]

# Walking up a parent chain: stop by raising StopIteration when there is no parent.
Node = Struct.new(:name, :parent)
root = Node.new("library")
floor = Node.new("first floor", root)
aisle = Node.new("aisle 7", floor)
path = Enumerator.produce(aisle) { it.parent || raise(StopIteration) }
p path.map(&:name) # => ["aisle 7", "first floor", "library"]

# Ruby 4.0: the size keyword. The default is Float::INFINITY, nil means "unknown".
p Enumerator.produce(1, &:succ).size # => Infinity
p Enumerator.produce(1, size: nil, &:succ).size # => nil
p(Enumerator.produce(3, size: 4) { it.zero? ? raise(StopIteration) : it - 1 }.size) # => 4
p(Enumerator.produce(1, size: -> { 42 }, &:succ).size) # => 42
puts(outcome { Enumerator.produce }) # => ArgumentError: no block given

Source: Enumerator.produce and the Ruby 4.0 NEWS (the size: keyword).

The unfold shape fits many real problems: the due dates of a recurring loan, a retry back-off (Enumerator.produce(1) { [_1 * 2, 60].min }), following parent or next links until nil, the Collatz or Newton iterations, and pagination by cursor. Combine it with lazy, take_while or first(n) to stop, and remember that without a stopping condition to_a on it never returns.

Lazy enumerators

Eager and lazy pipelines

A normal pipeline, (1..5).map { ... }.select { ... }.first(2), is eager: map finishes the whole collection and builds an intermediate Array, then select processes that Array and builds another, and only then does first(2) take two elements. That is wasteful when most results are thrown away, and impossible when the source is infinite. Enumerable#lazy returns an Enumerator::Lazy whose stages do not run when called. Each stage wraps the previous one, and a terminal call such as first(n), to_a or each pulls elements through the whole chain one at a time, stopping as soon as the terminal call is satisfied.

sequenceDiagram participant Src as Source 1 to 5 participant Map as map doubles participant Sel as select greater than 4 participant Out as first 2 Note over Src,Out: Eager, every stage finishes before the next one starts Src->>Map: 1, 2, 3, 4, 5 Map->>Sel: 2, 4, 6, 8, 10 Sel->>Out: 6, 8, 10 Out-->>Out: keeps 6 and 8 Note over Src,Out: Lazy, one element travels through all the stages Src->>Map: 1 Map->>Sel: 2 Sel-->>Map: rejected Src->>Map: 2 Map->>Sel: 4 Sel-->>Map: rejected Src->>Map: 3 Map->>Sel: 6 Sel->>Out: 6 Src->>Map: 4 Map->>Sel: 8 Sel->>Out: 8 Note over Out: two results, so 5 is never read

The example below records every call in a trace array. The eager run does five map calls and five select calls; the lazy run alternates them and stops after the fourth element. Building the lazy pipeline runs nothing at all (its trace stays empty, and inspect shows the nested stages map and select).

# frozen_string_literal: true

trace = []
double = lambda do |n|
  trace << "map #{n}"
  n * 2
end
big = lambda do |n|
  trace << "select #{n}"
  n > 4
end

# Eager: every stage finishes before the next one starts.
eager = (1..5).map(&double).select(&big).first(2)
p eager # => [6, 8]
p trace # => ["map 1", "map 2", "map 3", "map 4", "map 5", "select 2", "select 4", "select 6", "select 8", "select 10"]

# Lazy: one element travels through all the stages before the next one is fetched.
trace.clear
lazy = (1..5).lazy.map(&double).select(&big).first(2)
p lazy # => [6, 8]
p trace # => ["map 1", "select 2", "map 2", "select 4", "map 3", "select 6", "map 4", "select 8"]

# Building the pipeline runs nothing; only a terminal call does.
trace.clear
pipeline = (1..5).lazy.map(&double).select(&big)
p pipeline # => #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: 1..5>:map>:select>
p trace # => []
p pipeline.first(1) # => [6]
p trace # => ["map 1", "select 2", "map 2", "select 4", "map 3", "select 6"]
p pipeline.class # => Enumerator::Lazy
p pipeline.to_a # => [6, 8, 10]
p pipeline.force # => [6, 8, 10]

Creating and ending a lazy pipeline

  • enumerable.lazy starts a pipeline from anything that is Enumerable (an Array, a Range, a Hash, an IO, a Catalog, an Enumerator). Calling lazy on a lazy enumerator returns the same object.

  • Stage methods return another Enumerator::Lazy and compute nothing: map/collect, select/filter/find_all, reject, filter_map, flat_map/collect_concat, take, take_while, drop, drop_while, grep, grep_v, zip, uniq, compact (Ruby 3.1), with_index/each_with_index, chunk, chunk_while, slice_before, slice_after, slice_when, and each_slice/each_cons, which keep the pipeline lazy too.

  • Terminal methods run the pipeline: first (one element) and first(n), to_a/force (everything), each with a block, find, include?, next/peek, and every other Enumerable method that returns a value rather than an enumerator (sum, min, count, tally).

  • eager (Ruby 2.7) converts back to an ordinary Enumerator: the next stage called on it is no longer lazy, but it still pulls elements from the lazy chain on demand.

  • size is known only when no stage can change the number of elements: lazy, map and with_index keep it; select, reject, take_while and flat_map make it nil; take(n) bounds it.

  • Methods that need every element before they can answer (sort, sort_by, group_by, partition, tally, sum, each_with_object, reverse_each) return an Array or a value: the laziness ends there, and on an infinite source they never return.

# frozen_string_literal: true

def outcome
  yield.inspect
rescue StandardError => e
  "#{e.class}: #{e.message}"
end

naturals = (1..Float::INFINITY).lazy
p naturals # => #<Enumerator::Lazy: 1..Infinity>
p(naturals.map { it * 2 }.first(3)) # => [2, 4, 6]
p(naturals.select(&:even?).map { it * it }.first(3)) # => [4, 16, 36]
p naturals.reject(&:odd?).first(2) # => [2, 4]
p naturals.take(3) # => #<Enumerator::Lazy: #<Enumerator::Lazy: 1..Infinity>:take(3)>
p naturals.take(3).to_a # => [1, 2, 3]
p(naturals.take_while { it < 4 }.to_a) # => [1, 2, 3]
p naturals.drop(2).first(2) # => [3, 4]
p(naturals.drop_while { it < 5 }.first(2)) # => [5, 6]
p(naturals.filter_map { it * 10 if it.odd? }.first(3)) # => [10, 30, 50]
p(naturals.flat_map { [it, -it] }.first(4)) # => [1, -1, 2, -2]
p(naturals.zip(naturals.map { it * 100 }).first(2)) # => [[1, 100], [2, 200]]
p(naturals.with_index.map { |n, i| n * i }.first(3)) # => [0, 2, 6]
p naturals.each_slice(2).first(2) # => [[1, 2], [3, 4]]
p(naturals.chunk_while { |_, b| (b % 3).positive? }.first(2)) # => [[1, 2], [3, 4, 5]]
p(naturals.slice_when { |a, _| (a % 3).zero? }.first(2)) # => [[1, 2, 3], [4, 5, 6]]
p naturals.grep(2..4).first(3) # => [2, 3, 4]
p naturals.grep_v(2..4).first(3) # => [1, 5, 6]
p [3, 1, 3, 2, 1].lazy.uniq.to_a # => [3, 1, 2]
p [1, nil, 2, nil].lazy.compact.to_a # => [1, 2]
p naturals.lazy.equal?(naturals) # => true

# The terminal methods: first(n) and to_a/force evaluate; first without an argument returns one element.
p(naturals.map { it * 3 }.first) # => 3
p(naturals.find { it > 3 && it.even? }) # => 4
p naturals.include?(1000) # => true
p naturals.each_with_index.first(2) # => [[1, 0], [2, 1]]

# Not lazy: methods that need every element return an Array (or hang on an infinite source).
p((1..4).lazy.sort_by(&:-@)) # => [4, 3, 2, 1]
p((1..4).lazy.group_by(&:odd?)) # => {true => [1, 3], false => [2, 4]}
p((1..4).lazy.sum) # => 10
p((1..4).lazy.tally) # => {1 => 1, 2 => 1, 3 => 1, 4 => 1}
p((1..4).lazy.partition(&:even?)) # => [[2, 4], [1, 3]]
p((1..4).lazy.each_with_object([]) { |n, memo| memo << n }) # => [1, 2, 3, 4]

# eager turns it back into a plain Enumerator, so the next call is not lazy.
p((1..3).lazy.map { it * 2 }.eager) # => #<Enumerator: #<Enumerator::Lazy: #<Enumerator::Lazy: 1..3>:map>:each>
p((1..3).lazy.map { it * 2 }.eager.map { it + 1 }) # => [3, 5, 7]
p((1..3).lazy.map { it * 2 }.eager.class) # => Enumerator

# size is only known when no stage can change the number of elements.
p((1..3).lazy.size) # => 3
p((1..3).lazy.map { it }.size) # => 3
p((1..3).lazy.select { it }.size) # => nil
p((1..3).lazy.take(2).size) # => 2
p(naturals.map { it }.size) # => Infinity

Lazy over files and over the shelf

The best use of lazy is a source that is large, slow or unbounded: lines of a file or socket, pages of an API, rows of a query cursor, a generator. IO#each_line (and File.foreach) read on demand, so a lazy pipeline over them reads only as many lines as the consumer needs. The example parses a catalogue export held in a StringIO: asking for two books reads two lines (lineno shows it), and asking for all of them reads all five. Invalid lines are turned into nil by parse and removed by compact, and uniq with a block removes the repeated ISBN, all without building a single intermediate array.

# frozen_string_literal: true

require "shelf"
require "stringio"

# A catalogue export arriving as lines of "isbn;title;author". IO#each_line reads on demand,
# so a lazy pipeline over it never loads more lines than the consumer asks for.
export = StringIO.new(<<~LINES)
  9780441013593;Dune;Frank Herbert
  9780261103573;The Hobbit;J. R. R. Tolkien
  not-an-isbn;Broken;Nobody
  9780553293357;Foundation;Isaac Asimov
  9780441013593;Dune;Frank Herbert
LINES

def parse(line)
  digits, title, name = line.chomp.split(";")
  Shelf::Book.new(Shelf::Isbn.new(digits), title, Shelf::Author.new(name))
rescue ArgumentError
  nil
end

books = export.each_line.lazy.map { parse(it) }.compact.uniq(&:isbn)
p books.first(2).map(&:title) # => ["Dune", "The Hobbit"]
p export.lineno # => 2

export.rewind
p books.map(&:title).to_a # => ["Dune", "The Hobbit", "Foundation"]
p export.lineno # => 5

# A Catalog is Enumerable, so lazy works on it too, and Catalog#each without a block gives an enumerator.
catalog = Shelf::Catalog.new
export.rewind
books.each { catalog.add(it) }
p(catalog.lazy.select { it.title.length > 4 }.map(&:to_s).first(1)) # => ["The Hobbit by J. R. R. Tolkien"]
p(catalog.each.lazy.map { it.author.name }.to_a) # => ["Frank Herbert", "J. R. R. Tolkien", "Isaac Asimov"]

# Enumerator::Lazy.new builds a custom stage: the block receives a yielder and the incoming values.
def doubled(source)
  Enumerator::Lazy.new(source) { |yielder, value| yielder << (value * 2) }
end

p doubled([1, 2, 3]).first(2) # => [2, 4]
p(doubled(1..Float::INFINITY).select { (it % 3).zero? }.first(2)) # => [6, 12]

# The size argument keeps size information available.
p(Enumerator::Lazy.new([1, 2, 3], 3) { |yielder, value| yielder << value }.size) # => 3

Enumerator::Lazy.new(source, size = nil) { |yielder, *values| ... } builds a custom stage: the block receives the yielder and the incoming value for each element, and decides what to yield. It is rarely needed because map, select and flat_map cover nearly every case, but it is the primitive they are built from, and passing a size keeps size meaningful.

Pitfalls of laziness

Lazy evaluation changes when code runs, and that surfaces in a few predictable ways.

  • A lazy pipeline is a recipe, not a cache. Every terminal call runs the whole chain again from the source, so calling to_a twice calls the stages twice. Store the result of to_a if you need it more than once.

  • Side effects follow element order, not stage order. With two map stages that print, the output is a1 b1 a2 b2, not a1 a2 b1 b2. Code that relied on the first stage finishing for all elements before the second starts breaks when lazy is added.

  • An early exit never reaches later elements. That is the point, but a stage with a required side effect (updating a counter, closing a resource) will not run for elements that were never pulled.

  • Laziness ends at the first eager method. After sort_by or group_by you hold an Array.

  • An infinite source with a filter that never matches never ends. (1..).lazy.select { false }.first(1) loops forever; bound the search with take(n) or take_while first,.

  • It is not automatically faster. For a small collection every element passes through several layers of enumerator machinery, so a lazy chain is usually slower than the eager one that builds two small arrays. Use lazy when the source is infinite, large, expensive per element, or when the consumer stops early, and measure (see Performance, Benchmarking and Profiling) before turning it on "for speed".

  • with_index with a block is a side effect. On a lazy enumerator it calls the block for each element and passes the original element on; use with_index without a block and map to compute with the index.

# frozen_string_literal: true

calls = []
stage = (1..3).lazy.map do |n|
  calls << n
  n * 10
end

# Pitfall 1: a lazy pipeline is a recipe, not a cache; every terminal call runs it again.
p stage.to_a # => [10, 20, 30]
p stage.to_a # => [10, 20, 30]
p calls # => [1, 2, 3, 1, 2, 3]
cached = stage.to_a
p [cached.sum, cached.max] # => [60, 30]
p calls.size # => 9

# Pitfall 2: side effects inside stages happen in element order, not stage order.
log = []
(1..3).lazy
      .map { |n| log.push("a#{n}") && n }
      .map { |n| log.push("b#{n}") && n }
      .to_a
p log # => ["a1", "b1", "a2", "b2", "a3", "b3"]

# Pitfall 3: lazy with_index with a block runs the block per element as a side effect and passes elements on.
seen = []
p((1..3).lazy.with_index { |n, i| seen << [n, i] }.to_a) # => [1, 2, 3]
p seen # => [[1, 0], [2, 1], [3, 2]]
p((1..3).lazy.with_index(1).map { |n, i| n * i }.to_a) # => [1, 4, 9]

# Pitfall 4: laziness stops at the first eager method. Everything after sort or group_by is an Array.
p((1..6).lazy.map { it % 3 }.sort.class) # => Array
p((1..6).lazy.select(&:even?).map { it * 2 }.to_a.reverse) # => [12, 8, 4]

# An empty result from an infinite source never ends, so this call is shown but not executed:
# (1..Float::INFINITY).lazy.select(&:negative?).first(1)
p((1..Float::INFINITY).lazy.select(&:negative?).first(0)) # => []

Enumerator::Chain

first + second on an enumerator, Enumerable#chain(*others) (Ruby 2.6) and Enumerator::Chain.new(*enumerables) all build an Enumerator::Chain: an enumerator that iterates the first enumerable to its end, then the second, and so on. Nothing is concatenated in memory, and the parts can be of different kinds (an Array, a Range, an infinite generator). The + operator exists on Enumerator only; for an Array use chain, because Array#+ concatenates eagerly into an Array and expects an Array.

The size of a chain is the sum of the sizes of its parts when all of them are known, nil when one is unknown, and Infinity when one is infinite. Parts are started lazily: the second one is not touched until the first has been exhausted, as the log in the example shows. Calling chain on a lazy enumerator returns a lazy chain. A chain is built for internal iteration; next, peek and feed are undefined on it, and to_enum gives a plain cursor when you need one. rewind rewinds the parts (in reverse order) that respond to rewind.

# frozen_string_literal: true

fiction = %w[Dune Emma]
reference = %w[Atlas]

both = fiction.each + reference
p both # => #<Enumerator::Chain: [#<Enumerator: ["Dune", "Emma"]:each>, ["Atlas"]]>
p both.class # => Enumerator::Chain
p both.to_a # => ["Dune", "Emma", "Atlas"]
p both.size # => 3
p both.with_index(1).to_a # => [["Dune", 1], ["Emma", 2], ["Atlas", 3]]
p fiction.chain(reference, 1..2) # => #<Enumerator::Chain: [["Dune", "Emma"], ["Atlas"], 1..2]>
p fiction.chain(reference, 1..2).to_a # => ["Dune", "Emma", "Atlas", 1, 2]
p fiction.chain.to_a # => ["Dune", "Emma"]
p Enumerator::Chain.new(fiction, reference).to_a # => ["Dune", "Emma", "Atlas"]

# The size is the sum when every part knows its size, nil when one does not, Infinity when one is infinite.
p fiction.chain(reference).size # => 3
p(fiction.chain(Enumerator.new { |y| y << 1 }).size) # => nil
p fiction.chain(1..).size # => Infinity

# A chain is lazy about its parts: they are iterated one after the other and only as far as needed.
log = []
second = Enumerator.new do |y|
  log << :second_started
  y << 3
  y << 4
end
chain = [1, 2].each + second
p chain.first(2) # => [1, 2]
p log # => []
p chain.first(3) # => [1, 2, 3]
p log # => [:second_started]

# Enumerable#chain on a lazy enumerator keeps the result lazy.
p((1..Float::INFINITY).lazy.chain([0]).class) # => Enumerator::Lazy
p([1, 2].lazy.chain([3]).map { it * 2 }.to_a) # => [2, 4, 6]

# A chain is not a cursor: next, peek and feed are undefined on Enumerator::Chain. Wrap it to get one.
p both.respond_to?(:next) # => false
p both.respond_to?(:rewind) # => true
cursor = both.to_enum
p [cursor.next, cursor.next] # => ["Dune", "Emma"]
p cursor.peek # => "Atlas"

Enumerator.product

Enumerator.product(*enumerables) (Ruby 3.2) yields every combination, one element from each enumerable, as an Array, without building them all. The first enumerable is the outermost loop and the last one varies fastest, exactly like nested each calls and like Array#product. Enumerator::Product.new is the same thing as a constructor. It differs from Array#product in three ways: it is lazy, it accepts any object with each_entry (a Range, a Hash, a Set, an infinite generator), and its size is computed from the sizes of its arguments (Infinity if one is infinite, nil if one is unknown).

Edge cases follow the mathematics: with no arguments the product has exactly one element, the empty array []; with any empty argument it has none. A non-enumerable argument fails when the product is iterated (NoMethodError for each_entry), not when it is created. Remember that with an infinite first argument the later arguments are fully cycled before the first one advances, and with an infinite last argument the earlier ones never advance.

Known limitation: on Ruby 3.3, 3.4 and 4.0, calling with_index or each_with_index directly on an Enumerator::Product raises TypeError: wrong argument type product (expected enumerator). Wrap it first with to_enum or lazy as the last lines of the example do. Like Enumerator::Chain, a Product has no next.

# frozen_string_literal: true

def outcome
  yield.inspect
rescue StandardError => e
  "#{e.class}: #{e.message}"
end

formats = %i[paperback ebook]
languages = %w[en es]
editions = Enumerator.product(formats, languages)
p editions # => #<Enumerator::Product: [[:paperback, :ebook], ["en", "es"]]>
p editions.class # => Enumerator::Product
p editions.to_a # => [[:paperback, "en"], [:paperback, "es"], [:ebook, "en"], [:ebook, "es"]]
p editions.size # => 4
p(editions.map { |format, language| "#{format}/#{language}" }.first(2)) # => ["paperback/en", "paperback/es"]
p Enumerator.product(formats, languages, 1..2).first(2) # => [[:paperback, "en", 1], [:paperback, "en", 2]]

# Compare with Array#product: the array is built immediately, the enumerator is not.
p formats.product(languages) == editions.to_a # => true

# Edge cases: no arguments gives one empty combination; an empty argument gives none.
p Enumerator.product.to_a # => [[]]
p Enumerator.product(formats, []).to_a # => []
p Enumerator::Product.new(formats, languages).to_a == editions.to_a # => true

# The first enumerable is the outermost loop, and the last one varies fastest.
log = []
inner = Enumerator.new do |y|
  log << :inner_started
  y << :a
  y << :b
end
Enumerator.product([1, 2], inner).each { |number, letter| log << [number, letter] }
p log # => [:inner_started, [1, :a], [1, :b], :inner_started, [2, :a], [2, :b]]

# Infinite arguments: the size is Infinity and the combinations are produced on demand.
members = Enumerator.product(1.., %w[Ana Luis])
p members.first(3) # => [[1, "Ana"], [1, "Luis"], [2, "Ana"]]
p members.size # => Infinity
p Enumerator.product(1..3, 1..).first(4) # => [[1, 1], [1, 2], [1, 3], [1, 4]]

# The arguments only need each_entry, so a hash, a range or a Set all work.
p(Enumerator.product({ a: 1 }, Set[10, 20]).to_a) # => [[[:a, 1], 10], [[:a, 1], 20]]
puts(outcome { Enumerator.product(nil).to_a }) # => NoMethodError: undefined method 'each_entry' for nil

# On Ruby 3.3 to 4.0 with_index and each_with_index fail directly on a Product, so wrap it first.
puts(outcome { editions.with_index.to_a }) # => TypeError: wrong argument type product (expected enumerator)
p editions.to_enum.with_index(1).first(2) # => [[[:paperback, "en"], 1], [[:paperback, "es"], 2]]
p editions.lazy.each_with_index.first(2) # => [[[:paperback, "en"], 0], [[:paperback, "es"], 1]]

Enumerator::ArithmeticSequence

Numeric#step and Range#step (and its operator alias Range#%) return an Enumerator::ArithmeticSequence when called without a block on numbers (Ruby 2.6). Its inspect shows how it was built: (1.step(10, 3)), 1..10).step(3, 1..10).%(3, (1.step(by: 3, to: 10)). Unlike a generic enumerator it knows its begin, end, step and exclude_end?, can compute size, first(n), last(n), minmax and sum without iterating, compares equal to another sequence with the same parameters (== and eql?), is hashable, and is a full cursor (next, peek). It can also be used as an array index, which takes every n-th element (Ruby 3.0).

The step rules are those of step: a step of 0 raises ArgumentError: step can't be 0; a negative step walks downward and a step whose direction does not reach the end yields nothing; the sequence is endless when the end is omitted (1.step, (1..).step(2)), where size is Infinity and last raises RangeError; Float sequences compute each element as begin + (i * step) instead of accumulating, so they do not drift, and their size is computed with the same rounding-aware formula. With a block step iterates and returns the receiver. The finer points of Range#step (the Ruby 3.4 change to + for non-numeric ranges, String ranges) are in Ranges.

# frozen_string_literal: true

def outcome
  yield.inspect
rescue StandardError => e
  "#{e.class}: #{e.message}"
end

# Three ways to get an Enumerator::ArithmeticSequence (a number sequence with a fixed difference).
by_numeric = 1.step(10, 3)
by_range = (1..10).step(3)
by_percent = (1..10) % 3
by_keywords = 1.step(by: 3, to: 10)
p by_numeric # => (1.step(10, 3))
p by_range # => ((1..10).step(3))
p by_percent # => ((1..10).%(3))
p by_keywords # => (1.step(by: 3, to: 10))
p by_numeric.class # => Enumerator::ArithmeticSequence
p [by_numeric, by_range, by_percent, by_keywords].map(&:to_a).uniq # => [[1, 4, 7, 10]]
p by_numeric == by_range # => true
p by_numeric.eql?(by_range) # => true

# It answers questions about itself without iterating.
sequence = (1...20).step(4)
p [sequence.begin, sequence.end, sequence.step, sequence.exclude_end?] # => [1, 20, 4, true]
p [sequence.first, sequence.first(2), sequence.last, sequence.last(2), sequence.size] # => [1, [1, 5], 17, [13, 17], 5]
p sequence.to_a # => [1, 5, 9, 13, 17]
p [sequence.minmax, sequence.sum, sequence.include?(9)] # => [[1, 17], 45, true]

# Descending, endless and floating-point sequences.
p 10.step(1, -3).to_a # => [10, 7, 4, 1]
p 1.step(by: -1).first(3) # => [1, 0, -1]
p(1.step(by: 2).lazy.map { it * it }.first(3)) # => [1, 9, 25]
p 1.step.size # => Infinity
p 1.0.step(2.0, 0.25).to_a # => [1.0, 1.25, 1.5, 1.75, 2.0]
p 1.0.step(2.0, 0.1).size # => 11
p 1.0.step(2.0, 0.1).last # => 2.0
p 1r.step(2r, 1/3r).to_a # => [(1/1), (4/3), (5/3), (2/1)]

# Rules and errors.
p 3.step(1).to_a # => []
puts(outcome { 1.step(10, 0) }) # => ArgumentError: step can't be 0
puts(outcome { 1.step.last }) # => RangeError: cannot get the last element of endless arithmetic sequence
puts(outcome { (1..10).step("x").to_a }) # => TypeError: String can't be coerced into Integer
p(1.step(10, 3) { nil }) # => 1

# An ArithmeticSequence is a cursor too, unlike Chain and Product.
cursor = 1.step(10, 3)
p [cursor.next, cursor.next, cursor.peek] # => [1, 4, 7]

# Arrays accept a sequence as an index (Ruby 3.0) and take every n-th element.
shelves = %w[A B C D E F]
p shelves[(0..).step(2)] # => ["A", "C", "E"]
p shelves[(1..4).step(2)] # => ["B", "D"]
p shelves[(-1..0).step(-2)] # => ["F", "D", "B"]

Choosing the right tool

flowchart TD A["I need a sequence of values"] --> B{"Is the source a finite collection already in memory?"} B -->|"yes, one pass"| C["Use Array methods, map and select"] B -->|"yes, but large or I may stop early"| D["collection.lazy"] B -->|"no, it is computed"| E{"Does each value depend only on the previous one?"} E -->|"yes"| F["Enumerator.produce"] E -->|"no, it needs local state or I/O"| G["Enumerator.new with a yielder"] D --> H{"Do I need to pause and resume by hand?"} F --> H G --> H H -->|"yes, several sources in step"| I["next and peek on an external cursor"] H -->|"no"| J["each, first, take, to_a"]
Situation Use

Your class has an each

Return to_enum(__method__) without a block and include Enumerable.

Number sequences with a step

step, % and upto/downto; they are ArithmeticSequence objects that know their size.

Join several collections in order without copying

Enumerator, the plus method or Enumerable#chain.

All combinations of several lists

Enumerator.product (lazy) or Array#product (eager, builds the array).

Each value is a function of the previous one

Enumerator.produce.

A loop with local state or I/O that should look like a collection

`+Enumerator.new {

y

…​ }+`.

A big or infinite source and an early exit

lazy, and bound it with first(n), take or take_while.

Two sequences must advance together

External iteration: peek and next on two cursors, in one thread.

A paged source behind one enumerator

A common real use of an enumerator is hiding paging. The caller sees a flat stream of titles; the method fetches a page only when the previous one has been consumed. With first(3) only the pages needed are requested (two requests for pages of two), with a lazy select and first(2) the same two requests suffice, and to_a makes all four (the last page is empty and ends the stream). The same shape wraps a database cursor or a message queue. The last lines also show the identifier-generator idiom, produce for the sequence and next for the cursor, which is safe as long as the cursor stays in one thread.

# frozen_string_literal: true

# A paged remote catalogue simulated in memory: fetch_page counts how many "requests" were made.
class PagedSource
  PAGE_SIZE = 2

  attr_reader :requests

  def initialize(titles)
    @titles = titles
    @requests = 0
  end

  def fetch_page(number)
    @requests += 1
    @titles.each_slice(PAGE_SIZE).to_a.fetch(number - 1, [])
  end

  # Hide the paging behind one enumerator: callers see a flat, lazy stream of titles.
  def each_title(&)
    return to_enum(__method__) unless block_given?

    (1..).each do |number|
      page = fetch_page(number)
      break if page.empty?

      page.each(&)
    end
  end
end

source = PagedSource.new(["Dune", "The Hobbit", "Foundation", "Emma", "Ulysses"])
p source.each_title.first(3) # => ["Dune", "The Hobbit", "Foundation"]
p source.requests # => 2

source = PagedSource.new(["Dune", "The Hobbit", "Foundation", "Emma", "Ulysses"])
p(source.each_title.lazy.select { it.length > 4 }.map(&:upcase).first(2)) # => ["THE HOBBIT", "FOUNDATION"]
p source.requests # => 2

source = PagedSource.new(["Dune", "The Hobbit", "Foundation", "Emma", "Ulysses"])
p source.each_title.to_a.size # => 5
p source.requests # => 4

# An identifier generator: produce for the sequence, next for the cursor. Keep a cursor in one thread.
ids = Enumerator.produce(1000, &:succ)
book_ids = Array.new(3) { ids.next }
p book_ids # => [1000, 1001, 1002]
p ids.peek # => 1003

Edge cases and error behaviour

The next example collects the remaining behaviour of the core methods that is easy to get wrong: what each with a block returns on an enumerator, how feed sets the result of map, the errors of the constructors, what happens when a producer raises StopIteration, which offsets with_index accepts, and how rewind is forwarded to the receiver.

# frozen_string_literal: true

def outcome
  yield.inspect
rescue StandardError => e
  "#{e.class}: #{e.message}"
end

# Enumerator#each with a block calls the original method with that block, so the return value is the
# one of the underlying method. This is what makes map.with_index and select.with_index work.
titles = %w[Dune Emma Ulysses]
p titles.map.each(&:upcase) # => ["DUNE", "EMMA", "ULYSSES"]
p(titles.select.each { it.length > 4 }) # => ["Ulysses"]
p(titles.each_slice(2).each { nil }) # => ["Dune", "Emma", "Ulysses"]

# feed sets the value the next yield receives. For map that value becomes the mapped element.
mapper = titles.map
mapper.next
mapper.feed("D")
mapper.next
mapper.feed("E")
mapper.next
mapper.feed("U")
begin
  mapper.next
rescue StopIteration => e
  p e.result # => ["D", "E", "U"]
end
second = [1].each
second.feed(1)
puts(outcome { second.feed(2) }) # => TypeError: feed value already set

# Constructor errors.
puts(outcome { Enumerator.new }) # => ArgumentError: tried to create Proc object without a block
puts(outcome { Enumerator.new(1, 2) { nil } }) # => ArgumentError: wrong number of arguments (given 2, expected 0..1)
puts(outcome { [1].to_enum(:missing).to_a }) # => NoMethodError: undefined method 'missing' for an instance of Array

# A StopIteration raised inside the generator is not swallowed by to_a, only by loop.
broken = Enumerator.new do |y|
  y << 1
  raise StopIteration, "stopped early"
end
puts(outcome { broken.to_a }) # => StopIteration: stopped early
p broken.next # => 1
puts(outcome { broken.next }) # => StopIteration: stopped early

# with_index accepts any offset that converts to an Integer, and nil means 0.
p titles.each.with_index(10).to_a.last # => ["Ulysses", 12]
p titles.each.with_index(nil).to_a.last # => ["Ulysses", 2]
p titles.each_with_index.with_index(1).to_a.last # => [["Ulysses", 2], 3]

# rewind forwards to the receiver when it defines rewind.
class Tape
  def each
    yield :a
    yield :b
  end

  def rewind = puts("tape rewound")
end
reader = Tape.new.to_enum
p reader.next # => :a
reader.rewind # => tape rewound
p reader.next # => :a

Two behaviours deserve emphasis. A StopIteration raised inside the block of Enumerator.new is an ordinary exception for to_a and each (it propagates), but next also reports it as the end of iteration, and loop swallows it silently, so a bug in a producer can look like a short sequence. Raise StopIteration deliberately only inside Enumerator.produce (where it is the documented way to stop), and let Enumerator.new finish by returning from the block. And each_slice and each_cons with a block return the receiver since Ruby 3.1 (they returned nil before), which is what the each_slice(2).each { nil } line shows.

Common exceptions and warnings

All of these were reproduced on Ruby 4.0.7.

Error Message (4.0) Cause and fix

StopIteration

iteration reached an end

next or peek past the last element, or a StopIteration raised in a producer. Use loop, or rescue it and read result; rewind to start again.

FiberError

fiber called across threads

next or peek on an enumerator that another thread created or has already advanced. Create one enumerator per thread.

FrozenError

can't modify frozen Enumerator: #<Enumerator: [1]:each>

next, peek, rewind or feed on a frozen enumerator. Do not freeze cursors; internal iteration still works.

NoMethodError

undefined method 'next' for an instance of Enumerator::Chain

next, peek or feed on a Chain or a Product. Use to_enum or lazy to get a cursor.

NoMethodError

undefined method 'each_entry' for an instance of Integer

Enumerator.product given an argument that is not enumerable.

NoMethodError

undefined method 'missing' for an instance of Array

to_enum(:missing) is accepted, and the error appears when the enumerator is iterated.

ArgumentError

tried to create Proc object without a block

Enumerator.new or Enumerator::Lazy.new without a block.

ArgumentError

wrong number of arguments (given 2, expected 0..1)

The removed Enumerator.new(object, :method) form, or Enumerator.produce(initial, size: ...) on Ruby 3.4 and earlier.

ArgumentError

step can't be 0

1.step(10, 0) and (1..10).step(0).

ArgumentError

no block given

Enumerator.produce without a block.

TypeError

feed value already set

feed twice without a next in between.

TypeError

wrong argument type product (expected enumerator)

with_index or each_with_index directly on an Enumerator::Product (3.3 to 4.0).

RangeError

cannot get the last element of endless arithmetic sequence

last on 1.step or (1..).step(2).

LocalJumpError

no block given

Calling Enumerator::Yielder.new without a block; yielders are created by Enumerator.new, not by hand.

RuboCop cops for enumerators

These cops fired while the examples on this page were written and are worth knowing; all are enabled in the shelf configuration.

Cop What it checks

Style/ExplicitBlockArgument

A method that only wraps its block in another block (`+each {

x

yield x }+) should take `&block or the anonymous & and pass it on.

Style/ItBlockParameter

Prefers it to _1 in one-parameter blocks (Ruby 3.4).

Lint/AmbiguousBlockAssociation

p titles.select.with_index { ... } is ambiguous: write p(titles.select.with_index { ... }).

Style/ReduceToHash

An each_with_object({}) that only fills a Hash should be to_h { ... }.

Style/ParallelAssignment

a, b = 0, 1 should be two statements (the swap a, b = b, a + b is allowed).

Metrics/MethodLength

A generator with too many lines in one method; split the producer into small methods.

Style/NumericPredicate, Style/SymbolProc, Style/Semicolon

(n % 3).zero?, sort_by(&:-@) and no ; between statements in a block.

Where older books and tutorials are outdated

Books and articles written for Ruby 2.x or early 3.x still explain enumerators correctly in outline, but several details moved:

Older statement Current behaviour

"Use _1 for the block parameter" in map { _1 * 2 }

it (Ruby 3.4) is the idiom for one-parameter blocks, and RuboCop’s Style/ItBlockParameter prefers it. _1 is still needed when you use _1 and _2 together.

There is no way to build an infinite sequence from a seed except Enumerator.new with loop

Enumerator.produce (Ruby 2.7) says it directly, and Ruby 4.0 adds size:. Material that predates 2.7 does not mention it.

lazy is one-way: once lazy, you cannot go back

Enumerator::Lazy#eager (Ruby 2.7) returns an ordinary enumerator; force and to_a materialise an Array.

Enumerator, the plus method and Enumerable#chain do not exist, so concatenate with Array#+

Both exist since Ruby 2.6 and do not build an intermediate array; the result can be infinite.

All combinations need Array#product

Enumerator.product (Ruby 3.2) is lazy and works with infinite and non-Array sources.

step returns a plain Enumerator

It returns an Enumerator::ArithmeticSequence since 2.6, which knows size, last and equality, and which Array#[] accepts as an index since 3.0.

each_slice(2) { ... } returns nil

each_slice and each_cons return the receiver since Ruby 3.1.

Enumerator.new(object, :method) and Enumerator.new without a block

Raises ArgumentError. Use object.to_enum(:method).

Error messages quote the method name with a backtick and end in for nil:NilClass

Ruby 3.3 changed the receiver text and 3.4 the quote: undefined method 'x' for nil. The StopIteration message is unchanged.

inspect of a Hash or of a result shows {:a=>1}

Ruby 3.4 prints {a: 1} and {"Dune" => 4}, as in the output above.

Material that treats Enumerable and each as the whole of "the Iterator pattern" (including the shelf-style include Enumerable plus each) is still right; it just stops short of the fact that returning an enumerator when no block is given is what makes the iteration chainable.

Version notes

Ruby Enumerator changes

2.6

Enumerator::Chain, Enumerator, the plus method and Enumerable#chain; Enumerator::ArithmeticSequence returned by Numeric#step and Range#step; Enumerator::Lazy#filter alias.

2.7

Enumerator.produce; Enumerator::Lazy#eager; Enumerator::Yielder#to_proc; Enumerable#filter_map.

3.0

Array#[] accepts an Enumerator::ArithmeticSequence.

3.1

Enumerator::Lazy#compact; Enumerable#each_slice and each_cons return the receiver.

3.2

Enumerator.product and Enumerator::Product. Fiber[] storage, which external iteration inherits.

3.3, 3.4

No new Enumerator methods. 3.4: it as the implicit block parameter; Range#step uses + for every type; error messages use ' instead of a backtick.

4.0

Enumerator.produce accepts size: (an Integer, Float::INFINITY, a callable or nil).

4.1

Not released as of 2026-10-18 (expected 2026-12-25). Nothing from the development NEWS about enumerators is used on this page.