Feature #21520
openFeature Proposal: Enumerator::Lazy#lazy_each
Description
Abstract¶
Add a #lazy_each method to Enumerator::Lazy that allows observing each element in a lazy enumeration pipeline without modifying or consuming the stream.
Background¶
Ruby provides Enumerator::Lazy for efficient lazy stream processing. However, unlike languages such as Java, it lacks a clean way to inspect intermediate elements during lazy evaluation.
Currently, developers must misuse .map for side effects, example:
(1..).lazy.map { |x| puts x; x }.select(&:even?).first(3)
This is semantically incorrect and confusing, since .map implies transformation, not observation.
Proposal¶
Introduce Enumerator::Lazy#lazy_each, which yields each item to a block and returns the item unmodified, similar to Object#tap, but in a lazy stream:
(1..).lazy
.lazy_each { |x| puts "saw: #{x}" }
.select(&:even?)
.first(3)
This would be equivalent to:
lazy.map { |x| block.call(x); x }
but with improved semantic clarity.
Use cases¶
• Debugging lazy enumerators without breaking the chain
• Logging or instrumentation in pipelines
• Educational / demo use for showing lazy evaluation step-by-step
• Cleaner replacement for map { puts x; x } hacks
Example:
data = (1..).lazy
.lazy_each { |x| puts "got #{x}" }
.select(&:even?)
.first(5)
result:
got 1
got 2
got 3
got 4
got 5
...
got 10
And return [2, 4, 6, 8, 10]
Discussion¶
#lazy_each is a minimal, non-breaking addition that improves clarity and idiomatic usage of Enumerator::Lazy. It avoids abusing .map for observation and is familiar to developers from other languages. #lazy_each is also not needed for other enumerators where .tap or .each can do the job.
It mirrors Java’s .stream().peek(...) and makes Ruby’s lazy enumeration more expressive and readable.
See also¶
I have a draft PR for the implementation ready https://github.com/ruby/ruby/pull/14024
Updated by Dan0042 (Daniel DeLorme) 6 days ago
· Edited
#peek already exists, and does something different.
Updated by nuzair46 (Nuzair Rasheed) 6 days ago
Dan0042 (Daniel DeLorme) wrote in #note-2:
#peek already exists, and does something different.
Hey, Enumerator#peek exists. but Enumerator::Lazy#peek does not.
Enumerator#peek works differently than this suggestion.
So maybe the naming will be confusing.
Updated by nuzair46 (Nuzair Rasheed) 3 days ago
nuzair46 (Nuzair Rasheed) wrote in #note-3:
Enumerator#peek works differently than this suggestion.
So maybe the naming will be confusing.
Running the whole CI, I see the collision with Enumerator#peek.
So I think the name should be changed to avoid confusion and collision.
Im thinking spy
or tap_each
Updated by nuzair46 (Nuzair Rasheed) about 20 hours ago
- Subject changed from Feature Proposal: Enumerator::Lazy#peek to Feature Proposal: Enumerator::Lazy#lazy_each
- Description updated (diff)
updated to lazy_each