Lazy numerators work very similar to normal Ruby Enumberators but they don't execute until immediately. Instead they evaluate values on an as-needed basis. For a deep dive into how to use it, check the official documentation.
Thematically I like the idea of lazy evaluating enumerators quite a lot. It lines up well with ActiveRecord's lazy evaluation.
A basic usage example:
# Standard enumerator
(0..15).select { |i| i.even? }.map { |i| puts i; i *= 2 }.first(3)
0
2
4
6
8
10
12
14
=> [0, 4, 8]
# Lazy enumerator. Notice how we only manipulate our data until we have the first three qualifying elements.
(0..15).lazy.select { |i| i.even? }.map { |i| puts i; i *= 2 }.first(3)
0
2
4
=> [0, 4, 8]
Can we mimic the lazy behavior without using the lazy operator? Let's find out by attempting to find the first one hundred perfect squares between two large numbers. I'll wrap our code in some benchmarking to see how each performs.
iterations = 500
Benchmark.measure do
iterations.times do
# The "I don't know about lazy" way
(100000..50000000).each_with_object([]) do |i, array|
array << i if Math.sqrt(i) % 1 == 0
# We are too lazy to process beyond our desired count
break array if array.length == 100
end
end
end
# This took 5.598 seconds
Benchmark.measure do
iterations.times do
# The lazy way
(100000..50000000).lazy.select { |i| Math.sqrt(i) % 1 == 0 }.first(100)
end
end
# This took 8.290 seconds
Perhaps we're picking an unfair example, but 500 iterations of our lazy implementation is slower than a traditional implementation! Certainly we're doing better than a non-lazy implementation that doesn't break out of its loop, but the results are still somewhat surprising. A shame given how much nicer the lazy code is too.
Yusuke Endoh addressed this directly when lazy's performance issues were reported as a bug.
Enumerator::Lazy is not a silver bullet; it removes the overhead for creating an intermediate array, but brings the drawback for calling a block. Unfortunately, the latter is much bigger than the former. Thus, in general, Lazy does bring performance drawback.
The code cleanliness may be worth the small amount of additional overhead.
The proportional overhead of using the lazy enumerator decreases for slower chained operations. Consider the case where you're iterating over a list of items and making an HTTP GET for each until you find the object(s) that you're looking for. The overhead of those HTTP calls will far outweigh the performance hit from using a lazy enumerator.