Recreating Ruby’s Enumerable

include LearnByDoing

In this post, I’d like to dig into Ruby’s Enumerable module by building our own version, called MyEnumerable, with the map and select methods.

Enumerable basics

Ruby’s Enumerable module is indispensable for any Ruby programmer. It provides methods like each, select, and inject to collection objects.

From the Ruby Docs:

The Enumerable mixin provides collection classes with several traversal and searching methods, and with the ability to sort. The class must provide a method each, which yields successive members of the collection.

If you’ve used code that looks like this:

[1,2,3,4].select{|n| n.odd?}

or this:

[1,2,3,4].map{|n| n+1}

then you’ve used Enumerable. It’s important to realize that select and map are not instance methods of the Array class itself; they are methods that come with the Enumerable module, which is included by default in the Array class.

To use Enumerable in your own class, include it with a simple include Enumerable addition to the class definition, like so:

class MyClass
include Enumerable
  #...rest of code....must include an #each method
end

You must provide an #each method that can successively iterate through the elements of the MyClass collection object. Please read my other post on the ‘#each contract’ to learn more about why this is necessary.

Enumerable, how do you do it?

To better understand what’s happening under the hood, let’s build our own version of Enumerable with map and select. We’ll call it MyEnumerable.

We know that both map and select take blocks as arguments, so we can begin with a skeleton our MyEnumerable module:

module MyEnumerable
  def map(&block)
end
  def select(&block)
end
end

Implementing Map

The Ruby Docs define map as follows:

Returns a new array with the results of running block once for every element in enum.

So, when map is invoked on the collection object, it iterates through the collection and runs the block once for each element of the collection. The resulting values are returned in the same order in a new array. For example:

[1,2,3,4].map{|n| n+1}
#=> [2,3,4,5]

To implement our own version of map, let’s first create an empty array, which we’ll use to store the values returned from calling the block on each element of the collection object.

module MyEnumerable
  def map(&block)
result = []
end
end

To iterate successively through every member of the collection object, we can use the each method defined in the collection object’s class.

Array has an each method, for example, defined as:

Calls the given block once for each element in self, passing that element as a parameter.

Let’s add the each method to our map method:

module MyEnumerable
  def map(&block)
result = []
self.each do |element|
end
end
end

self here refers to the calling collection object. So, in our example earlier

[1,2,3,4].map{|n| n+1}

the collection object is the array [1,2,3,4] . This means that in MyEnumerable, self would be this [1,2,3,4] array.

Now that we can iterate over each element, let’s call the provided &block for each element, using that element as the argument to the block, and successively insert each returned value into the result array.

module MyEnumerable
  def map(&block)
result = []
self.each do |element|
result << block.call(element)
end
end
end

Now the result array will be [2,3,4,5]. The last step is to return the result array:

module MyEnumerable
  def map(&block)
result = []
self.each do |element|
result << block.call(element)
end
result
end
end

We’ve implemented a map method for our own MyEnumerable module. Let’s see how it works by using it with a FakeArrayWrapper class that will allow us to test array-like objects without falling back to the actual Array class. We have to use a fake array class for testing our MyEnumerable module; otherwise, if we called map on a real Array object, the map from the real Enumerable , and not from our fake MyEnumerable, would be called.

Here is our FakeArrayWrapper class. It contains the necessary each iteration method.

class FakeArrayWrapper
include MyEnumerable
  def initialize(*args)
@fake_array = args.flatten
end
  def each(&block)
@fake_array.each(&block)
self #return the original array
end
end

Calling map on an instance of the FakeArrayWrapper should work. Let’s give it a shot:

fake_array_instance = FakeArrayWrapper.new([1,2,3,4])
fake_array_instance.map {|n| n + 1}
#=> [2,3,4,5]
fake_array_instance.map {|n| n + 5}
#=> [6,7,8,9]
fake_array_instance.map {|n| 1}
#=> [1,1,1,1]

And it works as expected. To summarize, we

  1. Created a MyEnumerable module with a map method.
  2. The map method iterates over the calling object with the each method from the calling collection object’s class.
  3. For each element in the collection object, map calls the &block provided by the obj.map(&block) invocation, using the element as the argument to the block, and inserts the resulting value into a result array.
  4. When self.each has iterated over all of the collection object’s elements, map returns the result array.

Implementing Select

The Ruby Docs define select as follows:

Returns an array containing all elements of enum for which the given block returns a true value.

So, for example,

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

Implementing map in the last section gave us the general approach for writing our own methods in MyEnumerable:

  1. Iterate over the elements of the calling collection object using each, and call the provided block iteratively with the iterated element as the argument.
  2. Insert the resulting values into a result array inside the method.
  3. Return the result array.

So, for select, we will only include the elements of the collection object for which the called block returns true :

module MyEnumerable
  def select(&block)
result =[]
self.each do |element|
result << element if block.call(element) == true
end
result
end
end

Let’s give it a shot, again using the FakeArrayWrapper class:

class FakeArrayWrapper
include MyEnumerable
  def initialize(*args)
@fake_array = args.flatten
end
  def each(&block)
@fake_array.each(&block)
self #return the original array
end
end

Now calling select on instances of FakeArrayWrapper should work as expected:

fake_array_instance = FakeArrayWrapper.new([1,2,3,4])
fake_array_instance.select {|n| n==1}
# => [1]
fake_array_instance.select {|n| n.odd?}
# => [1,3]
fake_array_instance.select {|n| n.even?} 
# => [2,4]

Great!

Our simple MyEnumerable module now looks like

module MyEnumerable
  def map(&block)
result = []
self.each do |element|
result << block.call(element)
end
result
end
  def select(&block)
result =[]
self.each do |element|
result << element if block.call(element) == true
end
result
end
end

Both methods use each to iterate through the elements of the collection object that calls the method. The methods then insert the values returned from passing the iterated element as an argument into the provided block, into a result array. This result array is then the return value of calling map or select on a collection class object that includes MyEnumerable.

Thanks for reading.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.