Enumerators and Lazy Evaluation
Prerequisites: Comparable and Enumerable
Call most iteration methods without a block, and Ruby returns an Enumerator — an object that remembers how to iterate, ready to be chained or stepped through later.
enum = %w[a b c].each_with_index
p enum.class
p enum.map { |letter, i| "#{i}:#{letter}" }
p %w[x y z].map.with_index(1) { |v, i| "#{i}. #{v}" }
p [4, 5, 6].each_slice(2).to_a
p [1, 2, 3, 4].each_cons(2).map { |a, b| b - a }
Enumerator
["0:a", "1:b", "2:c"]
["1. x", "2. y", "3. z"]
[[4, 5], [6]]
[1, 1, 1]
map.with_index(1) is a common idiom: a map that also gives each
element's position, starting at 1.
Custom enumerators
Enumerator.new builds a sequence from a block. yielder << value
produces the next value — and the block only runs as far as someone
asks:
fibonacci = Enumerator.new do |y|
a, b = 0, 1
loop do
y << a
a, b = b, a + b
end
end
p fibonacci.take(10)
p fibonacci.next
p fibonacci.next
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
0
1
That loop is infinite, but the program doesn't hang: the enumerator
only computes values when they're requested.
lazy — chains that don't build intermediate arrays
Normally map and select each build a complete new array. On a huge
(or infinite) collection, that's slow or impossible. lazy processes
elements one at a time through the whole chain:
result = (1..Float::INFINITY).lazy
.map { |n| n * n }
.select(&:even?)
.first(4)
p result
[4, 16, 36, 64]
The same idea keeps memory flat when processing a large file:
errors = File.foreach("huge.log").lazy
.select { |line| line.include?("ERROR") }
.map(&:strip)
.first(10)
File.foreach without a block also returns an enumerator, so only as
many lines are read as needed to find ten errors.
Common mistakes
- Using
lazyon small arrays. It adds overhead per element; it's for large or infinite sources. - Forgetting to end a lazy chain with
first,to_a, orforce— without one, nothing is computed.