March 18, 2020
This blog is part of our Ruby 2.7 series.
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.
If this blog was helpful, check out our full blog archive.