Ruby 2.7 adds Enumerator::Lazy#eager


Enumerator::Lazy

Back in Ruby 2.0 added Enumerator::Lazy that allowed chaining of multiple operations to be performed on collection without evaluating them immediately, instead it evaluates only when its needed or forced to.

This allows us to perform operations on long or potentially infinite sequences and chain multiple operations without generating intermediate arrays.

2.5.3 :001 > prime_lazy = (2..Float::INFINITY).lazy.reject{|i| (2..Math.sqrt(i)).any?{|j| i % j == 0 } }
 => #<Enumerator::Lazy: #<Enumerator::Lazy: 2..Infinity>:reject>
 
2.5.3 :002 > prime_lazy = prime_lazy.select {|n| n > 100}
 => #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: 2..Infinity>:reject>:select>
 
2.5.3 :003 > prime_lazy.first(10)
 => [101, 103, 107, 109, 113, 127, 131, 137, 139, 149]

As we can see, here we created a lazy enumerator which returns prime numbers greater than 100. When its evaluated by calling first, each value of i is processed through all the operations in the chain before processing the next element in collection.

Also, each operation added to the enum, except the last one, of calling first where we force the enum to return a result, it returns another instance of lazy enum.

Need for eager in lazy:

Sometimes we may want to generate a normal Enumerator while we still want to have the advantage of lazy operations.

Like for example, some method expects an enum as argument and the caller code is expecting to receive an array as the result. This may be a case when working with method from a gem that expects an enum as argument, so passing a lazy enum may require a change to implementation of the method or the calling code by calling force or to_a that forces the enum to evaluate.

head :001 > # Suppose this is a method that excepts enum
head :002 > def process(enum)
head :003 >   enum.select { |element| element < 100 }
head :004 > end
 => :process
 
head :005 > # The caller is expecting an array as the result
head :006 > result = process((0..10).lazy.map {|x| x ** 2})
head :007 > # Throws error as `shuffle` is not defined on enum but on an array
head :008 > result.shuffle
Traceback (most recent call last):
        5: from /Users/narendra/.rvm/rubies/ruby-head/bin/irb:23:in `<main>'
        4: from /Users/narendra/.rvm/rubies/ruby-head/bin/irb:23:in `load'
        3: from /Users/narendra/.rvm/rubies/ruby-head/lib/ruby/gems/2.7.0/gems/irb-1.1.0.pre.3/exe/irb:11:in `<top (required)>'
        2: from (irb):7
        1: from (irb):4:in `process'
NoMethodError (undefined method `shuffle' for #<Enumerator::Lazy:0x00007fbd61158580>)

head :009 > result = process((0..10).lazy.map {|x| x ** 2})
head :010 > # We either need to force evaluate the result or change the process method to return an array
head :011 > result.force.shuffle
 => [25, 36, 4, 9, 81, 64, 0, 16, 49, 1]

Ruby 2.7 adds Enumerator::Lazy#eager

To overcome some of these force ing situations, Ruby 2.7 has added eager method to Enumerator::Lazy. It returns a normal enumerator from the lazy one.

head :001 > result = process((0..10).lazy.map {|x| x ** 2}.eager)
head :002 > result.shuffle
 => [16, 1, 0, 9, 81, 64, 4, 25, 49, 36]

Here the lazy enumerator is evaluated in an eager way, and passed to process and then shuffle which expect to work on a normal enumerator.