Ruby Tuesday – Enumerable#lazy

Today’s Ruby Tuesday in on Enumerable#lazy.

Enumerable#lazy makes an enumerable so that it will evaluate only as a value is needed.

If we take a benchmark of calculating the squares of the first ten numbers in the range of one to one-million, we can see the time difference.

Benchmark.realtime{(1..1_000_000).map{|x| x^2}.take(10) }
# => 0.187007
Benchmark.realtime{(1..1_000_000).lazy.map{|x| x^2}.take(10) }
# => 3.2e-05

As we only end up taking the first ten results, the timing difference shows that we are only performing the square on the items needed when the range was declared as lazy, as opposed to calculating the squares of all of the numbers and taking the first ten of that result.

We can also see the effect when we take two infinite sequences, and try to zip them together, and print the first ten items.

abcs = [:a, :b, :c].cycle
# => #<Enumerator: ...>
 nums = [1, 2, 3].cycle
# => #<Enumerator: ...>
abcs.zip(nums).take(10).each{|x| puts x.inspect}
# ^CInterrupt:
# from (pry):58:in `next'

You can sit there all day, and more likely, all your life, at your Ruby prompt waiting for it to return, but it has to do the zip of two infinite sequences before it can take the first ten items.

If we make those infinite sequences lazy, we will zip those items together until only for as long as we need the data to complete the pipeline of operations, and it returns the result seemingly instantaneously.

lazy_abcs = [:a, :b, :c].cycle.lazy
# => #<Enumerator::Lazy: ...>
lazy_nums = [1, 2, 3].cycle.lazy
# => #<Enumerator::Lazy: ...>
lazy_abcs.zip(lazy_nums).take(10).each{|x| puts x.inspect}
# [:a, 1]
# [:b, 2]
# [:c, 3]
# [:a, 1]
# [:b, 2]
# [:c, 3]
# [:a, 1]
# [:b, 2]
# [:c, 3]
# [:a, 1]
# => nil

By using lazy, it not only allows us to save computation cycles avoiding work that would never be needed, but it can also open up new possibilities to structuring problems in a way that would never be able to be finished without lazy enumerables.

–Proctor