This blog is part of our Ruby 2.7 series. Ruby 2.7.0 was released on Dec 25, 2019.

Ruby 2.0 introduced Enumerator::Lazy, a special type of enumerator which helps us in processing chains of operations on a collection without actually executing it instantly.

By applying Enumerable#lazy method on any enumerable object, we can convert that object into Enumerator::Lazy object. The chains of actions on this lazy enumerator will be evaluated only when it is needed. It helps us in processing operations on large collections, files and infinite sequences seamlessly.

# This line of code will hang and you will have to quit the console by Ctrl+C.
irb> list = (1..Float::INFINITY).select { |i| i%3 == 0 }.reject(&:even?)

# Just adding `lazy`, the above line of code now executes properly
# and returns result without going to infinite loop. Here the chains of
# operations are performed as and when it is needed.
irb> lazy_list = (1..Float::INFINITY).lazy.select { |i| i%3 == 0 }.reject(&:even?)
=> #<Enumerator::Lazy: ...>

irb> lazy_list.first(5)
=> [3, 9, 15, 21, 27]

When we chain more operations on Enumerable#lazy object, it again returns lazy object without executing it. So, when we pass lazy objects to any method which expects a normal enumerable object as an argument, we have to force evaluation on lazy object by calling to_a method or it’s alias force.

# Define a lazy enumerator object.
irb> list = (1..30).lazy.select { |i| i%3 == 0 }.reject(&:even?)
=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>

# The chains of operations will return again a lazy enumerator.
irb> result = list.select { |x| x if x <= 15 }
=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>:map>

# It returns error when we call usual array methods on result.
irb> result.sample
irb> NoMethodError (undefined method `sample'
irb> for #<Enumerator::Lazy:0x00007faab182a5d8>)

irb> result.length
irb> NoMethodError (undefined method `length'
irb> for #<Enumerator::Lazy:0x00007faab182a5d8>)

# We can call the normal array methods on lazy object after forcing
# its actual execution with methods as mentioned above.
irb> result.force.sample
=> 9

irb> result.to_a.length
=> 3

The Enumerable#eager method returns a normal enumerator from a lazy enumerator, so that lazy enumerator object can be passed to any methods which expects a normal enumerable object as an argument. Also, we can call other usual array methods on the collection to get desired results.

# By adding eager on lazy object, the chains of operations would return
# actual result here. If lazy object is passed to any method, the
# processed result will be received as an argument.
irb> eager_list = (1..30).lazy.select { |i| i%3 == 0 }.reject(&:even?).eager
=> #<Enumerator: #<Enumerator::Lazy: ... 1..30>:select>:reject>:each>

irb> result = eager_list.select { |x| x if x <= 15 }
irb> result.sample
=> 9

irb> result.length
=> 3

The same way, we can use eager method when we pass lazy enumerator as an argument to any method which expects a normal enumerator.

irb> list = (1..10).lazy.select { |i| i%3 == 0 }.reject(&:even?)
irb> def display(enum)
irb>   enum.map { |x| p x }
irb> end

irb>  display(list)
=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>:map>

irb> eager_list = (1..10).lazy.select { |i| i%3 == 0 }.reject(&:even?).eager
irb> display(eager_list)
=> 3
=> 9

Here’s the relevant commit and feature discussion for this change.