How I Think About Ruby’s Enumerable

Call the lawyers; it’s a contract.

Andrew Livingston
3 min readFeb 14, 2017

I’d like to give a high level demonstration of how Ruby’s Enumerable module requires an #each method from classes that include it.

I heard Armando Fox, an EECS professor at UC Berkeley, describe Enumerable as a ‘contract’ between the class that includes it and the module itself. That is, Enumerable makes the following deal with the classes that include it:

“If you provide me with a way to iterate, or enumerate, over each instance of yourself, I will give you various useful methods in return.”

The way classes provide this iteration to Enumerable is through an #each method. It’s a contract that exchanges #each for use of the methods in Enumerable. This includes map, select, and inject.

From The Ruby Docs:

The class must provide a method #each, which yields successive members of the collection.

It makes sense that for a class to use Enumerable, the class must provide an #each method; otherwise, Enumerable wouldn’t know how to successively access each element in a collection.

Diving Into The Contract

Let’s see what happens when a class does not provide an #each method and an instance of the class tries to call select and map. To do this, we’ll create a FakeArrayWrapper class that omits the #each method. We can think about FakeArrayWrapper as a wrapper on the Array class.

class FakeArrayWrapper  include Enumerable  def initialize(*args)
@fake_array = args.flatten
end

#we omit the #each method here
end

When we try to call anEnumerable method, select, on an instance ofFakeArrayWrapper, the invocation returns an error.

fake_array_instance = FakeArrayWrapper.new([1,2,3,4])fake_array_instance.select {|n| n == 1} 
# =>
fake_array.rb:17:in `select': undefined method `each' for #<FakeArrayWrapper:0x007fed5e816c00 @fake_array=[1, 2, 3, 4]> (NoMethodError)
from fake_array.rb:17:in `<main>'

Similarly, we get an error when calling map :

fake_array_instance = FakeArrayWrapper.new([1,2,3,4])fake_array_instance.map {|n| n + 1}
# =>
fake_array.rb:20:in `map': undefined method `each' for #<FakeArrayWrapper:0x007f87908179e0 @fake_array=[1, 2, 3, 4]> (NoMethodError)
from fake_array.rb:20:in `<main>'

These NoMethodError errors tell us that select and map from Enumerable tried to use a method called #each on the calling object (in this case fake_array_instance), but that this method doesn’t exist for instances of FakeArrayWrapper.

We knew this already; we didn’t define #each in the FakeArrayWrapper class.

Ruby was saying here:

“I tried to call select and map on your fake_array_instance, but in order to do that, I needed you to give me an #each method for the fake_array_instance.”

Note that the FakeArrayWrapper class does not change the behavior of the real Array class. Calling the same Enumerable methods on a real Array instance still returns the expected:

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

Now, if we implement the #each method in FakeArrayWrapper :

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

the Enumerable methods return the expected values for the fake_array_instance :

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

We no longer receive the NoMethodError because now FakeArrayWrapper#each is implemented, and select and map can call fake_array_instance.each(&block) without a problem.

So, in order for classes including Enumerable to use Enumerable’s methods, the including class must provide an #each method that allows Enumerable to iterate over the class instance’s elements.

In another post, I implement my own version of Enumerable, called MyEnumerable, with map and select, to shed some light on the inner workings of Enumerable.

--

--