Ruby

Project: Custom Enumerables

Ruby Course

Introduction

You should be very familiar with the Enumerable module that gets mixed into the Array and Hash classes (among others) and provides you with lots of handy iterator methods. To prove that there’s no magic to it, you’re going to rebuild those methods.

We will explain an example of how an enumerable works to give you an idea of how to start tackling these methods. Specifically we’re going to break down the #find enumerable method. The #find method finds the first element that matches the given block’s condition and returns it. If one isn’t found, it returns nil.

a = [1, 2, 3, 4]
a.find { |n| n == 2 }
# results in `2`

a.find { |n| n == 10 }
# results in `nil`

Now the question is how would we rebuild this method using our knowledge of yield and blocks? Let’s go over the example shown below line by line.

module Enumerable
  def my_find
    self.each do |elem|
      return elem if yield(elem)
    end

    nil
  end
end

a = [1, 2, 3]
a.my_find { |n| n == 2 }
#=> 2

First of all, we’re doing something you’ve maybe not seen before here: manipulating an existing class/module in the Ruby language. Ruby allows you to do this. We can reopen the Enumerable module and add our custom methods there.

With self.each, we’re calling the #each method on the object instance that’s invoking this method. In the example, this will end up being an array. So self will refer to the array that’s calling #my_find. We can then use the #each method to iterate through its elements. Now this is where yield becomes extremely useful. When called inside of the #my_find method, yield will give control to the block that has been provided for #my_find. In the usage example just below the definition, we can see the { |n| n == 2 } block is passed to the #my_find method. Inside of #my_find, each element in the array gets yielded to that block as an argument.

If the block returns true/truthy for an element, we immediately return that element. If nothing is found, we’ll iterate all the way through the array and end up executing the nil return at the very end. Pretty cool, huh?

Another thing you may not be familiar with: the Enumerator class. A lot of Enumerable methods return an Enumerator instance if a block isn’t passed in. We can see from the documentation that Enumerable#find is no different. This allows Enumerable methods to be chained in a clean and performant way. Fortunately, creating an Enumerator is quite straightforward. You can just make use of the to_enum method.

module Enumerable
  def my_find
    # returns an `Enumerator` bound to this method if a block isn't given
    return to_enum(:my_find) unless block_given?

    self.each do |elem|
      return elem if yield(elem)
    end

    nil
  end
end

a = [2, 3, 4, 5, 6, 7, 9]

# find the first element that's odd at an even index
a.my_find.with_index { |num, idx| num.even? && idx.odd? }
#=> 9

This is the power of Enumerators. If #find could only ever return a result, we would have no way to combine it with other methods through chaining like that.

Now it’s time for you to practice:

Assignment

  1. Fork and clone our custom enumerables repo
  2. Follow the installation instructions in the README to get the repo setup locally.
  3. Rebuild each of the methods in the table at the end of the README and make sure they all pass the tests associated with them.

Additional resources

This section contains helpful links to related content. It isn’t required, so consider it supplemental.

Support us!

The Odin Project is funded by the community. Join us in empowering learners around the globe by supporting The Odin Project!