DEV Community

Cover image for Ruby's magical Enumerable module
Jeff Kreeftmeijer for AppSignal

Posted on • Updated on • Originally published at blog.appsignal.com

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.

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

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>
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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]]]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb(main):003:0> list.count
=> 3
irb> list.max
=> 73
irb> list.map { |item| item * item }
=> [1764, 144, 5329]
irb> list.select(&:even?)
=> [42, 12]
Enter fullscreen mode Exit fullscreen mode

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
  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
Enter fullscreen mode Exit fullscreen mode

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(main):226:0> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb(main):227:0> list.each
=> #<Enumerator: [42, [12, [73, nil]]]:each>
irb(main):228:0> list.map.with_index.to_h
=> {42=>0, 12=>1, 73=>2}
Enter fullscreen mode Exit fullscreen mode

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. If you liked this article, check out the entire series on Ruby Magic. Magicians never share their secrets. But we do.

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 leave a comment.

Top comments (2)

Collapse
 
blankfella profile image
blank

Hi, thanks for the Enumarator explanation really liked the article. Just wanted to oint out a typo here:
"...We pass :each, to tell the as that's the method the enumerator..."

I think there is a missing word here. Have a good day.

Collapse
 
jkreeftmeijer profile image
Jeff Kreeftmeijer

Great find! In fact, those were extra words! Updated. :)