An Enumerator Class in Ruby

Juzer Shakir
4 min readJul 25, 2021

--

Rich source of methods provided by Ruby available to use by enumerator objects.

Prerequisite

This topic will be easier to comprehend if you have the following knowledge:

Topics covered in this article

Enumerator objects instantiate when we pass no block to an Enumerable method, that object becomes an instance of an Enumerator class. Hence it returns something like this:

#<Enumerator: [1, 2, 3]:delete_if>

Definition:

An enumerator class which allows both internal and external iteration. — Official Ruby Docs

Internal iteration:

An internal iteration is an iteration where logic is passed to the block to an enumerable method.
For example:

[2, 7, 10, 6, 9].count { |num| num % 3 == 0 } 
# => 2

Here logic is passed to ablock as{ |num| num % 3 == 0 } to the count method. This is an internal iteration of Enumerator Class.

External iteration:

An external iteration is an iteration where logic is not passed to the block to an enumerable method but to some other method later in the program.
For example:

arr =  (1..50).each  # => #<Enumerator: 1..50:each>

This is an external iteration of Enumerator Class.

Now with this external iteration, an Enumerator class provides methods to inspect element of it with these following methods:

p Enumerator.instance_methods(false)
# => [:rewind, :with_index, :with_object, :next_values, :peek_values, :peek, :feed, :next, :inspect, :, +, :each_with_index, :each_with_object, :size, :each]

In the above list of methods, the ones with bold methods are the ones that manipulate the internal position of the external iteration.

next: Returns the next object in the enumerator, and moves the internal position forward.
peek: Returns the next object in the enumerator, but doesn’t move the internal position forward.
rewind: Moves internal position of the enumeration sequence to the beginning.
peek_values: They function similar to the peek method, but return the value within an array.
next_values: They function similar to the next method, but return the value within an array.

An instance of an Enumerator Class

As you can see there are multiple ways to instantiate an instance of an Enumerator class.

2nd & 3rd way uses to_enum & enum_for methods respectively. Both methods are from theKernel module. An Array doesn’t have this method in them so it goes up in the ancestor chain of Array to find these methods.

By giving to_num & enum_for method to an Array, we generator an Enumerator object or instance of an Enumerator Class.

In enum_2, we passed thenext method as an argument to to_enum, but we can pass any methods from Enumerable module or Enumerator class. The reason methods are available of Enumerable module in Enumerator class its because Enumerator class has ‘include'd Enumerable module in it. Here next method is from Enumerator class.

If we don’t pass any argument to to_enum & enum_for, by default it uses each method.

In enum_4, we have used theEnumerator::new method passed with a block as its necessary. This method returns an object with an instance of a Enumerator::Generator class. Hence the output of it is different compared to others.

Now, let’s take a look at how methods of the Enumerator class work under the hood:

p [1, 2, 3, 4].select { |ele| ele.even? }
# => [2, 4]

This extracts only even numbers from the list. So select method loops through each element in the list and executes the block and returns the element for which the block returned true.

Now, let's take this same concept and implement it as an Enumerator.

p [1, 2, 3, 4].select
#<Enumerator: [1, 2, 3, 4]:select>

When Enumerable module method select is passed without a block, it returns an instance of Enumerator class. If a block was passed it would iterate over each element with each method, execute the block and return the results. We can say that Enumerator is like a pending iteration, waiting to happen later.

And now we can give any methods from Enumerable module and Enumerator class to perform an operation.

enum_obj = [0, 4, 2, 1].selectp enum_obj.each_with_index { |ele, index|  ele == index }
# => [0, 2]

We use each_with_index method from Enumerator class provided with a block which takes 2 arguments.

What happens in the above program is that each_with_index starts the pending iteration with the block provided. Since the pending iteration uses select method, the enumerator will call Array#select. The array then passes each element back to the Enumerator object enum_obj which passes them into the block along with the index in each_with_index method.

Chaining an Enumerator

Nested Enumerators. An Enumerator within an Enumerator.

The above program is similar to one we saw previously, the only difference is that instead of providing the block to the second method, with_index, we provide it to the third method, each.

Classes in an Enumerator class

There are 6 classes defined in an Enumerator class, they’re as follows:

  1. ArithmeticSequence
  2. Chain
  3. Lazy
  4. Producer
  5. Yielder
  6. Generator

Enumerator class includes an Enumerable module in it which makes all the methods of module available to the Enumerator class.

Enumerator.ancestors
# => [Enumerator, Enumerable, Object, Kernel, BasicObject]

--

--