The classic iterator pattern describes a way of accessing the elements of an aggregate object without exposing its implementation. This pattern comes in two flavors: external and internal. An external iterator is controlled by the client, while an internal iterator is controlled by the aggregate object.
In Ruby, internal iteration is the norm. Ruby’s
Enumerable module adds several traversal, searching, and sorting algorithms to the main collection classes,
Hash. While working with
Enumerable you may have come across
Enumerable‘s close relative.
Enumerator is an
Enumerable plus external iteration. In this post, we’ll take a look at the basics of
Enumerators and some of the powerful functionality that they make possible.
Creating an Enumerator from a Block
One way to create an
Enumerator is by passing a block to its constructor. A yielder object will be passed to this block. The yielder’s
#<< method can be used to define the elements the
Enumerator will iterate through.
Enumerator#next can then be used to provide a controlled, external iteration.
Enumerator#next is called, the
Enumerator will proceed to the next yielded value. It will
then yield the value, giving control back to the caller. On the subsequent call to
Enumerator will resume execution up to the next yielded value. That's how an
Enumerator maintains its current position in the iteration. After the last yielded value, a
StopIteration error will be raised. Interestingly,
StopIteration is rescued by
Creating an Enumerator from an Enumerable
The more common way to create an
Enumerator is from an
Enumerable object, specifically an object that defines an
Object#to_enum (aliased to
Object#enum_for) is implemented to return a new
Enumerator that will enumerate by sending
#each to its receiver.
Enumerator itself is
Enumerable, so we can use the full power of
Object#to_enum takes a method name to generate the
Enumerator. The default is
#each. Passing in an alternative method can result in some strange behavior.
In this example, we created an
Enumerator based on
Array#map. This causes
Enumerator#each to act like
Enumerator#with_index was then used to create a
Creating an Enumerator from a Blockless Enumerable Method Call
Enumerable methods that take a block will return an
Enumerator when called without a block. For example, calling
Array#select without a block will return an
Enumerator with an
#each method that will filter like
#select. These blockless calls offer a concise alternative to
Turning Non-Enumerables into Enumerables
So far, we've looked at how to construct
Enumerators from scratch and from
Enumerables. However, several non-
Enumerable classes also return
Enumerable, this allows you to effectively turn a non-
Enumerable class into an
Integer, for example, returns an
String has several iteration methods that will return an
Enumerator when not given a block.
Enumerator's external iteration can be used to construct infinite lists.
Enumerator will yield one value at a time. It pauses after each yield. When asked for another value, it will resume immediately after the last yielded value, and execute up to the next yielded value. In the above example, we used
Kernel#loop to prevent the enumeration from ever stopping.
One problem with infinite lists is that an internal iteration method, for example
#select, will hang forever. This is because methods like
#map rely on filtering/transforming the entire collection, i.e., they'll ask for the entire infinite list.
Continuing with our above infinite list example:
Lazy versions of iternal iteration methods provide a solution.
Enumerator#lazy_select returns a new
Enumerator that will yield each of its values that the given block returns true for.
Enumerable#first is necessary to limit the number of results.
Enumerators seem to be everywhere in Ruby's standard library. External iteration, infinite lists, and lazy iteration, while interesting, are probably not something you'll ever use. However, an
Enumerator's ability to turn non-
Enumerables can result in some pretty elegant code. Ruby 1.9 put
Enumerators on center stage by integrating them tightly with
Enumerable is integral to just about every Ruby program, it's important to know what's possible with its close relative