Logo of AppSignal

Menu

Ruby Magic

Ruby's magical Enumerable module

Jeff Kreeftmeijer on

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.

1
2
3
4
5
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.

1
2
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.

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
10
11
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.

1
2
3
4
5
6
7
8
9
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.

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.

[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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

1
2
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
2
3
4
5
6
7
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LinkedList
  include Enumerable

  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
1
2
3
4
5
6
7
8
9
10
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LinkedList
  include Enumerable

  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.

1
2
3
4
5
6
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!

Latest Ruby Magic articles (see all)

10 latest articles

Go back

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.

We'd like to set cookies, read why.