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.