Lazy enumerators
An infinite range
In Ruby, (1..Float::INFINITY) is a range with no end. Without lazy, any method that walks it runs forever; with lazy, a method that asks for three items gets three items and stops:
ruby -e 'p (1..Float::INFINITY).lazy.map { _1 * 2 }.first(3)'
[2, 4, 6]
The chain is one item at a time: take one from the range, double it, hand it to first, repeat. Nothing after the third item is ever computed, which is the whole idea of a lazy enumerator.
take and take_while
take(n) asks for n items and take_while { } asks until a condition breaks. On an infinite lazy range both are safe, and first is the same call with n = 1:
ruby -e 'p (1..100).lazy.select(&:even?).take(3).to_a'
[2, 4, 6]
ruby -e 'p [1, 2, 3, 10].take_while { _1 < 5 }'
[1, 2, 3]
take_while stops at the first item that fails, so the ten after it is never seen. That differs from select, which would keep going and return [1, 2, 3] only because ten is not smaller than five.
Eager against lazy
On a finite collection the two forms print the same answer, and the difference is memory: the eager version builds the whole array before first takes one item from it.
ruby -e 'p (1..100).select(&:even?).first(3); p (1..100).lazy.select(&:even?).first(3).to_a'
[2, 4, 6]
[2, 4, 6]
With a hundred items the eager version allocates a hundred-element array for three answers. With a log of a million lines read through File.foreach, the lazy chain is the difference between a script that runs in constant memory and one that does not.
What to watch for
A lazy chain has to end in something that consumes it:
first, take,
to_a or each. A
p of the lazy object prints the enumerator, not
the items, which surprises the first time.
Lazy is not free: each item goes through a chain of blocks, so a small
collection is faster in the eager form. Reach for
lazy when the source is large or infinite, not by
habit.
Next: Methods
Article author: Arthur Isaev