Ruby’s magical Enumerable module

It’s time for another episode of Ruby Magic! This time, we’ll look at one of Ruby’s most magical features, which provides most of the methods you’ll use when working with Ruby’s enumerable classes like Array, Hash and Range. In the process, we’ll learn what you can do with enumerable objects, how enumeration works, and how to make an object enumerable by implementing a single method.

AppSignal
5 min readMay 29, 2018

Enumerable, #each and Enumerator

Enumeration refers to traversing over objects. In Ruby, we call an object enumerable when it describes a set of items and a method to loop over each of them.

The built-in enumerables get their enumeration features by including the Enumerable module, which provides methods like #include?, #count, #map, #select and #uniq, amongst others. Most of the methods associated with arrays and hashes aren’t actually implemented in these classes themselves, they’re included.

Note: Some methods, like #count and #take on the Array class, are implemented specifically for arrays instead of using the ones from the Enumerable module. That’s usually done to make operation faster.

The Enumerable module relies on a method named #each, which needs to be implemented in any class it’s included in. When called with a block on an array, the #each method will execute the block for each of the array’s elements.

irb> [1,2,3].each { |i| puts "* #{i}" }
* 1
* 2
* 3
=> [1,2,3]

If we call the #each method on an array without passing a block to execute for each of its elements, we’ll receive an instance of Enumerator.

irb> [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>

Instances of Enumerator describe how to iterate over an object. Enumerators iterate over objects manually and chain enumeration.

irb> %w(dog cat mouse).each.with_index { |a, i| puts "#{a} is at position #{i}" }
dog is at position 0
cat is at position 1
mouse is at position 2
=> ["dog", "cat", "mouse"]

The #with_index method is a good example of how changed enumerators work. In this example, #each is called on the array to return an enumerator. Then, #with_index is called to add indices to each of the array’s elements to allow printing each element’s index.

Making objects enumerable

Under the hood, methods like #max, #map and #take rely on the #each method to function.

def max
max = nil

each do |item|
if !max || item > max
max = item
end
end
max
end

Internally, Enumerable’s methods have C implementations, but the example above roughly shows how #max works. By using #each to loop over all values and remembering the highest, it returns the maximum value.

def map(&block)
new_list = []
each do |item|
new_list << block.call(item)
end
new_list
end

The #map function calls the passed block with each item and puts the result into a new list to return after looping over all values.

Since all methods in Enumerable use the #each method to some extent, our first step in making a custom class enumerable is implementing the #each method.

Implementing #each

By implementing the #each function and including the Enumerable module in a class, it becomes enumerable and receives methods like #min, #take and #inject for free.

Although most situations allow falling back to an existing object like an array and calling the #each method on that, let’s look at an example where we have to write it ourselves from scratch. In this example, we’ll implement #each on a linked list to make it enumerable.

In Ruby, a linked list can be created by using a class that holds two instance variables named @head and @tail.

class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end

def <<(item)
LinkedList.new(item, self)
end

def inspect
[@head, @tail].inspect
end
end

The #<< method is used to add new values to the list, which works by returning a new list with the passed value as the head, and the previous list as the tail.

In this example, the #inspect method is added so we can see into the list to check which elements it contains.

irb> LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]

Now that we have a linked list, let’s implement #each on it. The #each function takes a block and executes it for each value in the object. When implementing it on our linked list, we can use the list’s recursive nature to our advantage by calling the passed block on the list’s @head, and calling #each on the @tail, if it exists.

class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end

When calling #each on an instance of our linked list, it calls the passed block with current @head. Then, it calls each on the linked list in @tail unless the tail is nil.

irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.each { |item| puts item }
42
12
73
=> nil

Now that our linked list responds to #each, we can include Enumberable to make our list enumerable.

class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end

def <<(item)
LinkedList.new(item, self)
end

def inspect
[@head, @tail].inspect
end

def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.count
=> 3
irb> list.max
=> 73
irb> list.map { |item| item * item }
=> [1764, 144, 5329]
irb> list.select(&:even?)
=> [42, 12]

Returning Enumerator instances

We can now loop over all values in our linked list, but we can’t chain enumerable functions yet. To do that, we’ll need to return an Enumerator instance when our #each function is called without a block.

class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end

def <<(item)
LinkedList.new(item, self)
end

def inspect
[@head, @tail].inspect
end

def each(&block)
if block_given?
block.call(@head)
@tail.each(&block) if @tail
else
to_enum(:each)
end
end
end

To wrap an object in an enumerator, we call the #to_enum method on it. We pass :each, as that’s the method the enumerator should be using internally.

Now, calling our #each method without a block will allow us to chain enumeration.

irb> list = LinkedList.new(73) << 12 << 42 => [42, [12, [73, nil]]] irb> list.each
=> #<Enumerator: [42, [12, [73, nil]]]:each>
irb> list.map.with_index.to_h
=> {42=>0, 12=>1, 73=>2}

Nine lines of code and an include

By implementing #each using the Enumerable module and returning Enumerator objects from our own, we were able to supercharge our linked list by adding nine lines of code and an include.

This concludes our overview of enumerables in Ruby. We’d love to know what you thought of this article, or if you have any questions. We’re always on the lookout for topics to investigate and explain, so if there’s anything magical in Ruby you’d like to read about, don’t hesitate to let us now at @AppSignal!

Originally published at blog.appsignal.com on May 29, 2018.

--

--

AppSignal
AppSignal

Written by AppSignal

Error tracking and performance insights for Ruby and Elixir without the silly per-host pricing. From Amsterdam with love.

No responses yet