Ruby Tips 10 - Adding enumeration to your class

One of the cool things in Ruby is to be able to pass a block to a method or go through all the elements in a class (such as an array or hash) and do something as you step through each element. This is a short post on how to add an each method to your class.

Delegating Enumeration

If enumeration is already provided by a member of your class, the process is really simple.

Let’s take a class (called EachOne) like this, for simplicity:

1
2
3
4
5
class EachOne
  def initialize
    @elements = [1, 44, 5, 7, 2, 6, 7]
  end
end

So, this has an array called elements. We could, of course, make elements available to the outside world by using attr_reader :elements so that we can iterate on that directly but this exposes the internal implementation. The better way is to add a method called each for objects of EachOne but get that method to instead iterate through the array.

Since we want it to make it enumerable, we need to do 2 things:

  • Add include Enumerable to bring the Enumerable module into your class
  • Add a method called each that, in turn, calls the method on elements

So, this becomes:

1
2
3
4
5
6
7
8
9
10
class EachOne
  include Enumerable
  def initialize
    @elements = [1, 44, 5, 7, 2, 6, 7]
  end

  def each(&block)
    @elements.each(&block)
  end
end

For many cases, this will be enough. We receive &block as a parameter on the each method and we just call the each method on the elements array, passing it the &block as an argument.

Implementing Enumeration

We won’t implement a custom data structure in this post but I strongly recommend reading https://blog.appsignal.com/2018/05/29/ruby-magic-enumerable-and-enumerator.html which has a well-written post on this topic.

Using the ideas from that post, we’ll implement the enumeration on the elements array as custom code rather than just passing the block to the each method on elements.

What we want to achieve is this:

  • Have a method called each that takes the block as a parameter
  • The method goes through all the elements and calls the block (using call) passing each item to it

In this case, the each method becomes as below.

1
2
3
4
5
  def each(&block)
    @elements.each do |x|
      block.call(x)
    end
  end

This works for the cases where you want to pass a block but we need to return an Enumerator instance when each is called without a block. As explained in the AppSignal post, we need to:

  • Wrap the object in an enumerator by calling to_enum(:each) on it
  • Add a check using block_given? to decide whether we should enumerate and call the block, or return the object wrapped in an enumerator

The code then becomes as below.

1
2
3
4
5
6
7
8
9
  def each(&block)
    if block_given?
	  @arr.each do |x|
	    block.call(x)
	  end
    else
      to_enum(:each)
    end
  end

I wrote this up so that I could remember how this is done. If it helps someone else, that’s great!

Additional reading

As mentioned above, I recommend reading this page on Ruby’s magical Enumerable module that I also take reference from. On blocks in general, the bootrails post on blocks, procs and lambda is an easy-to-understand post.

comments powered by Disqus