Ruby 2.7 adds Enumerator::Lazy#eager

Ashik Salman

Ashik Salman

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.

1# This line of code will hang and you will have to quit the console by Ctrl+C.
2irb> list = (1..Float::INFINITY).select { |i| i%3 == 0 }.reject(&:even?)
3
4# Just adding `lazy`, the above line of code now executes properly
5# and returns result without going to infinite loop. Here the chains of
6# operations are performed as and when it is needed.
7irb> lazy_list = (1..Float::INFINITY).lazy.select { |i| i%3 == 0 }.reject(&:even?)
8=> #<Enumerator::Lazy: ...>
9
10irb> lazy_list.first(5)
11=> [3, 9, 15, 21, 27]
12

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.

1
2# Define a lazy enumerator object.
3irb> list = (1..30).lazy.select { |i| i%3 == 0 }.reject(&:even?)
4=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>
5
6# The chains of operations will return again a lazy enumerator.
7irb> result = list.select { |x| x if x <= 15 }
8=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>:map>
9
10# It returns error when we call usual array methods on result.
11irb> result.sample
12irb> NoMethodError (undefined method `sample'
13irb> for #<Enumerator::Lazy:0x00007faab182a5d8>)
14
15irb> result.length
16irb> NoMethodError (undefined method `length'
17irb> for #<Enumerator::Lazy:0x00007faab182a5d8>)
18
19# We can call the normal array methods on lazy object after forcing
20# its actual execution with methods as mentioned above.
21irb> result.force.sample
22=> 9
23
24irb> result.to_a.length
25=> 3
26

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.

1
2# By adding eager on lazy object, the chains of operations would return
3# actual result here. If lazy object is passed to any method, the
4# processed result will be received as an argument.
5irb> eager_list = (1..30).lazy.select { |i| i%3 == 0 }.reject(&:even?).eager
6=> #<Enumerator: #<Enumerator::Lazy: ... 1..30>:select>:reject>:each>
7
8irb> result = eager_list.select { |x| x if x <= 15 }
9irb> result.sample
10=> 9
11
12irb> result.length
13=> 3
14

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

1irb> list = (1..10).lazy.select { |i| i%3 == 0 }.reject(&:even?)
2irb> def display(enum)
3irb>   enum.map { |x| p x }
4irb> end
5
6irb>  display(list)
7=> #<Enumerator::Lazy: #<Enumerator::Lazy: ... 1..30>:select>:reject>:map>
8
9irb> eager_list = (1..10).lazy.select { |i| i%3 == 0 }.reject(&:even?).eager
10irb> display(eager_list)
11=> 3
12=> 9
13

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

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.