Understanding Lazy Enumerables

Let’s do a brief recap on terminology before we dive into lazy enumerables. What’s the difference between an enumerable and an enumerator?

In Ruby, an Enumerable is a collection class (such as Array and Hash) that contains methods for traversal, searching, and sorting.

An Enumerator, on the other hand, is an object that performs the actual enumeration. There are two kinds of enumeration—internal and external—which will be explained in External vs. Internal Iteration.

Lazy enumeration was introduced in Ruby 2.0. What exactly is lazy? Is Ruby trying to slack off? Well, the “lazy” in lazy enumeration refers to the style of evaluation. To understand this better, let’s review the opposite of lazy evaluation: eager evaluation.

You’re already familiar with eager evaluation, as that’s the usual way most Ruby code is written. But sometimes, as in life, being overly eager is a bad thing. For instance, what do you think the following code evaluates to?

 >>​ 1.upto(Float::INFINITY).map { |x| x * x }.take(10)

You might expect the result to be:

 [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Unfortunately, the Ruby interpreter just goes on and on infinitely. The main offender is this piece:

 1.upto(Float::INFINITY)

1.upto(Float::INFINITY) represents an infinite sequence:

 >>​ 1.upto(Float::INFINITY)
 =>​ ​#<Enumerator: 1:upto(Infinity)>

No surprise here; that expression returns an enumerator. Note that no results are returned at this point, just a representation of an infinite sequence. Now, let’s try to force values out from the enumerator using the Enumerator#to_a method. Try it on a small and finite sequence first:

 >>​ 1.upto(5).to_a
 =>​ [1, 2, 3, 4, 5]

Now, repeat the same method call, but this time on the infinite sequence:

 >>​ 1.upto(Float::INFINITY).to_a

You shouldn’t be surprised by now that this will lead to an infinite loop. Enumerator.to_a “forces” values out of an enumerator. As an interesting side note, the to_a is aliased to force. This method is useful when you want to know all the values produced by an enumerator. You will be using this method later on.

So how can you convince Ruby not to evaluate every single value? Enter Enumerable#lazy. This method creates a lazy enumerable. Now, to infinity and beyond:

 >>​ 1.upto(Float::INFINITY).lazy.map { |x| x * x }
 =>​ ​#<Enumerator::Lazy: #<Enumerator::Lazy:
  #<Enumerator: 1:upto(Infinity)​>>​​:map​>

With lazy, the 1.upto(Float::INFINITY) enumerator has been made lazy by being wrapped up in an Enumerator::Lazy class, which has a lazy version of map.

Let’s try the very first expression that caused the infinite computation, but this time with Enumerable#lazy:

 >>​ 1.upto(Float::INFINITY).lazy.map { |x| x * x }.take(10)
 =>​ ​#<Enumerator::Lazy: #<Enumerator::Lazy:
  #<Enumerator::Lazy:
  #<Enumerator: 1:upto(Infinity)​>>​​:map​>​:take​(10)>

What just happened? Instead of getting ten values, it turns out that even Enumerable#take is wrapped up! How can you get your values then? Enumerable#to_a to the rescue:

 >>​ 1.upto(Float::INFINITY).lazy.map { |x| x * x }.take(10).to_a
 =>​ [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Why is Enumerable#take also wrapped up? This lets you do more lazy chaining. It allows you to hold off getting values out of the enumerator up until the point where you really need the values.

How does this sorcery work behind the scenes? You are about to find out. You will create your own version of lazy enumerables, albeit a minimalistic version. Along the way, you will also learn interesting aspects of Ruby’s enumerators that you probably wouldn’t have known about. Let’s get started.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset