ruby

Ruby's magical Enumerable module

Jeff Kreeftmeijer

Jeff Kreeftmeijer on

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.

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.

1irb> [1,2,3].each { |i| puts "* #{i}" }
2* 1
3* 2
4* 3
5=> [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.

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

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

1irb> %w(dog cat mouse).each.with_index { |a, i| puts "#{a} is at position #{i}" }
2dog is at position 0
3cat is at position 1
4mouse is at position 2
5=> ["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.

1def max
2  max = nil
3
4  each do |item|
5    if !max || item > max
6      max = item
7    end
8  end
9
10  max
11end

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.

1def map(&block)
2  new_list = []
3
4  each do |item|
5    new_list << block.call(item)
6  end
7
8  new_list
9end

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.

Linked lists: lists without arrays

A linked list is a collection of data elements, in which each element points to the next. Each element in the list has two values, named the head and the tail. The head holds the element’s value, and the tail is a link to the rest of the list.

1[42, [12, [73, nil]]

For a linked list with three values (42, 12 and 73), the first element’s head is 42, and the tail is a link to the second element. The second element’s head is 12, and the tail holds the third element. The third element’s head is 73, and the tail is nil, which indicates the end of the list.

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

1class LinkedList
2  def initialize(head, tail = nil)
3    @head, @tail = head, tail
4  end
5
6  def <<(item)
7    LinkedList.new(item, self)
8  end
9
10  def inspect
11    [@head, @tail].inspect
12  end
13end

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.

1irb> LinkedList.new(73) << 12 << 42
2=> [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.

1class LinkedList
2  def initialize(head, tail = nil)
3    @head, @tail = head, tail
4  end
5
6  def <<(item)
7    LinkedList.new(item, self)
8  end
9
10  def inspect
11    [@head, @tail].inspect
12  end
13
14  def each(&block)
15    block.call(@head)
16    @tail.each(&block) if @tail
17  end
18end

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.

1irb> list = LinkedList.new(73) << 12 << 42
2=> [42, [12, [73, nil]]]
3irb> list.each { |item| puts item }
442
512
673
7=> nil

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

1class LinkedList
2  include Enumerable
3
4  def initialize(head, tail = nil)
5    @head, @tail = head, tail
6  end
7
8  def <<(item)
9    LinkedList.new(item, self)
10  end
11
12  def inspect
13    [@head, @tail].inspect
14  end
15
16  def each(&block)
17    block.call(@head)
18    @tail.each(&block) if @tail
19  end
20end
1irb> list = LinkedList.new(73) << 12 << 42
2=> [42, [12, [73, nil]]]
3irb> list.count
4=> 3
5irb> list.max
6=> 73
7irb> list.map { |item| item * item }
8=> [1764, 144, 5329]
9irb> list.select(&:even?)
10=> [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.

1class LinkedList
2  include Enumerable
3
4  def initialize(head, tail = nil)
5    @head, @tail = head, tail
6  end
7
8  def <<(item)
9    LinkedList.new(item, self)
10  end
11
12  def inspect
13    [@head, @tail].inspect
14  end
15
16  def each(&block)
17    if block_given?
18      block.call(@head)
19      @tail.each(&block) if @tail
20    else
21      to_enum(:each)
22    end
23  end
24end

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.

1irb> list = LinkedList.new(73) << 12 << 42
2=> [42, [12, [73, nil]]]
3irb> list.each
4=> #<Enumerator: [42, [12, [73, nil]]]:each>
5irb> list.map.with_index.to_h
6=> {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!

Share this article

RSS

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps