Enumerating collections the Ruby way
Tags: ruby, enumerable, enumerator, enumerating
Published: 2022-03-13 11:03:06

---
title: Enumerating collections the Ruby way
published: true
description: Enumerate over a custom collection class using what Ruby provides. No more rolling your own each method that takes in a block  with the wonderful & often magical Enumerable module.
tags: ruby,enumerable,enumerator,enumerating
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqnkak0pjpmdvzaepmjg.png
---

Originally posted at my [blog](https://mutatedbread.space/blogs/2022-03-13/enumerating_collectings_the_ruby_way).

---

[Enumeration](https://en.wikipedia.org/wiki/Enumeration) is the complete and ordered listing/traversing of all items in a collection. To list over something in Ruby, most of us may start with something like this:

```ruby
class Collection
  def initialize(items)
    @items = items
  end

  def each(&block)
    @items.each { |item| block.(item) }
  end

  # OR
  def each(&block)
    @items.each(&block)
  end
end
```

A good follow question will be can we now use the `#map` and `#reduce` that we all love? We will be horrified to learn that we need to implement them by hand if we follow the way above!

Fortunately, we can implement our own with the [`Enumerable`](https://ruby-doc.org/core-3.0.2/Enumerable.html) module, as do by the `Hash`, `Array` and `Range` classes. By including the `Enumerable` module in our collection class and implementing the `#each` method, we will get access to all methods that has an element of listing over elements such as `#map`, `#reduce` and `#each_slice`.

## Example

There are two booths in a data collection exercise. The first collects a person's name and the second collects the person's hobbies. After filling up the entire list, the organizer wants to traverse over a person's name and hobbies together. An array of names and a two-dimensional array of hobbies are readied in the same sequence. We can imagine these requirements as a `NameHobbiesColletion` class to list over a person's name and hobbies in sequence.

```ruby
class NameHobbiesCollection
  include Enumerable

  def initialize(names, array_of_hobbies)
    @names = names
    @array_of_hobbies = array_of_hobbies
  end

  # This will be the only method we need to implement for traversal.
  def each(&block)
    @names.zip(@array_of_hobbies).each do |name, hobbies|
      block.(name, hobbies)
    end
  end
end
```

### Testing Them Out

1. Let's setup an instance and see some enumeration methods in action!

    ```ruby
    collection = NameHobbiesCollection.new(
      %w(James Joe Tony),
      [
        %w(Talking Baking),
        %w(Science Coding Talking),
        %w(Coffee)
      ]
    )
    ```

2. We can use `#each` to list over the collecitons and do things:

    ```ruby
    collection.each do |name, hobbies|
      puts "#{name} has hobbies of #{hobbies.join(', ')}"
    end
    # => James has hobbies of Taking, Baking
    # => Joe has hobbies of Science, Coding, Talking
    # => Tony has hobbies of Coffee
    ```

3. We can use `#map` to produce an array of human-readable logs:

    ```ruby
    collection.map do |name, hobbies|
      "name:#{name} hobbies:[#{hobbies.join(',')}]"
    end
    # => [
    # =>   "name:James hobbies:[Talking,Baking]",
    # =>   "name:Joe hobbies:[Science,Coding,Talking]",
    # =>   "name:Tony hobbies:[Coffee]"
    # => ]
    ```

4. `#partition` allows us to group chatty people together and the others together:

    ```ruby
    collection.partition do |_name, hobbies|
      hobbies.include? 'Talking'
    end
    # => [
    # =>   [["James", ["Talking", "Baking"]], ["Joe", ["Science", "Coding", "Talking"]]],
    # =>   [["Tony", ["Coffee"]]]
    # => ]
    ```

5. We can use `#each_slice` to iterate over the collection in chunks, useful for persisting items to the database in batches instead of everything at once:

    ```ruby
    collection.each_slice(2).to_a
    # => [
    # =>   [["James", ["Talking", "Baking"]], ["Joe", ["Science", "Coding", "Talking"]]],
    # =>   [["Tony", ["Coffee"]]]
    # => ]
    ```

## Can we make it chainable?

If we want to chain methods like `collection.map.with_index`, the colletion is expected to return an [`Enumerator`](https://ruby-doc.org/core-3.0.3/Enumerator.html). To do that, we can rely on the [`Object#to_enum`](https://www.rubydoc.info/stdlib/core/Object:to_enum) method to create an enumerator listing over the collection using a specified method from it. Any arguments should be past over.

```ruby
  def each(*args, &block)
    # When there is no block given, we return an Enumerator that points to this each method!
    return to_enum(__method__, *args) unless block_given?

    # Implementation follows here when there is a block.
  end
```

### Testing them out

1. Chaining `#each` with `#with_index`:
    ```ruby
    collection.each.with_index do |(name, hobbies), index|
      puts "queue #{index}: #{name} has hobbies of #{hobbies.join(', ')}"
    end
    # => queue 0: James has hobbies of Talking, Baking
    # => queue 1: Joe has hobbies of Science, Coding, Taking
    # => queue 2: Tony has hobbies of Coffee
    ```
2. Iterating over the collection with an arbitrary object with `#each` and `#with_object` and returns it:

    ```ruby
    collection.each.with_object({}) do |(name, hobbies), hash|
      hash[name] = hobbies
    end
    # => {
    # =>   "James"=>["Talking", "Baking"],
    # =>   "Joe"=>["Science", "Coding", "Talking"],
    # =>   "Tony"=>["Coffee"]
    # => }
    ```

3. Emitting the entire collections:
    ```ruby
    collection.each.entries
    collection.each.to_a
    # => [
    # =>  ["James", ["Talking", "Baking"]],
    # =>  ["Joe", ["Science", "Coding", "Talking"]],
    # =>  ["Tony", ["Coffee"]]
    # => ]
    ```

## References

`Enumerator` includes the `Enumerable` module. Explore more methods availbale in the `Enumerator` module below:

1. Enumerable: https://ruby-doc.org/core-3.0.2/Enumerable.html
2. Enumerator: https://ruby-doc.org/core-3.0.3/Enumerator.html