Episode #059: Enumerator

What is an Enumerator, anyhow? What are they good for? … And how are they different from Enumerable?

In this episode, you will learn how Enumerators work, and why you need this handy tool in your toolbox.

Upgrade to download episode video.

Episode Script

I've mentioned Enumerator a couple of times in past episodes, but I thought today we might focus a little more closely on what they are and what they're good for.

First of all, let's be clear: we're talking about the Enumerator class, not the Enumerable module. They are related, but they are not the same.

So what's an Enumerator? Let's start by defining a simple method. All this method does is yield a series of names, one after another. It gets each name by shifting them off the top of an array.

@names = %w[Ylva Brighid Shifra Yesamin]

def names
  yield @names.shift
  yield @names.shift
  yield @names.shift
  yield @names.shift
end

When we call this method we pass it a block. The method yields to the block four times, once for each name.

names do |name|
  puts name
end

I want to draw attention to the fact that this method consists of straight-line code. It executes a series of statements, one after another. There is no looping or iteration going on here.

Now let's create an Enumerator for this method. To do that, we call to_enum and pass it the name of the method.

enum = to_enum(:names)
#<Enumerator: main:names>

Now let's see what we can do with this Enumerator.

An Enumerator is an external iterator. So one thing we can do is ask it for the next item.

enum.next
Ylva

We get back the first name in the list. So what has happened here? Looking at the result, we might hypothesize that the method has been executed, and now all the values that were yielded are being given back to us one by one. Let's check this hypothesis by inspecting the @names array.

@names

The first name is gone, but the rest remain. Consider what this means: the names method executed as far as the first yield, and then stopped. If it had proceeded any further, there wouldn't be three names left in the array.

OK, what happens when we call #next again?

enum.next
"Brighid"

We get another name back. When we inspect the @names array, we see it is down to two names.

@names

This means that the #names method advanced one more line, and then stopped again. It is frozen in time, waiting for another call to #next before it proceeds again.

There are two lines of code left in the #names method. What happens when we call #next three more times?

enum.next
enum.next
enum.next rescue $!
#<StopIteration: iteration reached an end>

The last call to #next raises a StopIteration exception. This is the end of the line; there's no more method left to execute.

Unless, that is, we load up some more names and then backtrack to the beginning of the method by calling #rewind.

@names = %w[Kashti Aodhan Edgar Heinlein]
enum.rewind
enum.next

So from what we've seen so far, we can say that an Enumerator takes a method that yields multiple times, and effectively turns it inside out: instead of the method driving execution by yielding repeatedly to a block, we can drive the method's execution forward as needed by calling #next on the Enumerator.

One handy method Enumerator includes is #with_index. This method lets us iterate through the yielded values with an added index variable. This variable automatically counts up with each yield. We can use this to number the names:

enum.with_index do |name, index|
  puts "#{index}: #{name}"
end

What other tricks does enumerator have up its sleeve? If we take a look at its ancestry, we can see that it includes the Enumerable module. So while Enumerator and Enumerable are not the same, an Enumerator is Enumerable.

Enumerator.ancestors

Enumerable objects normally support the #each method. Let's see what #each does on our enum object.

enum.each do |name|
  puts name
end

That's interesting. It's not very useful though; for that, we could have just called our #names method directly and never bothered with an Enumerator.

Of course, Enumerable objects support a lot of other operations as well. One of the most basic is #to_a.

enum.to_a

We've taken a method that yields values, and constructed an array of those yielded values. This is starting to look a little more promising; it means we can easily collect an array from any method that yields, without fiddling around with blocks appending values to some temporary variable.

We can also use methods like #detect to search through the results.

enum.detect{|n| n =~ /^B/}
Brighid

Notice that since #detect returns as soon as it finds a match, the method has not finished executing. There are still names left in the array:

@names

There are lots more Enumerable methods we could demonstrate here, but I think you probably get the idea. An Enumerator takes any method that yields values to a block, and turns it into a lazy, iterable object which supports all the convenient methods of Enumerable. And that's pretty darned handy.

Happy hacking!