An Enumerator Class in Ruby
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:
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:
ArithmeticSequence
Chain
Lazy
Producer
Yielder
Generator
Enumerator
class include
s an Enumerable
module in it which makes all the methods of module available to the Enumerator
class.
Enumerator.ancestors
# => [Enumerator, Enumerable, Object, Kernel, BasicObject]