Implement a custom Enumerable collection class in Ruby

Implement a custom Enumerable collection class in Ruby
by Darren Jensen | Programming | January 2015

Background

The Enumerable module in Ruby Core is very powerful. It provides methods such as sort, min, max and others for working specifically with collections. Its implemented by collection classes such as Array out of the box but what if you need to make your own custom collection class? In this case you will need to include the Enumerable module. Here’s how.

An Enumerable Coffee Shop

Lets consider a Coffee Shop that has a number of different blends of coffee on offer. Imagine that the shop wants to sort these different blends by strength. Also consider that we may need to quickly determine the stongest or the weakest coffee. Enumerable module to the rescue!

The item class first (Coffee Class)

First of all, lets get some coffee brewed by implementing our Coffee class!

class Coffee

  attr_accessor :name

  attr_accessor :strength



  def initialize(name, strength)

    @name = name

    @strength = strength

  end

end

Lets make the coffee!

laos = Coffee.new("Laos", 10)

angkor = Coffee.new("Angkor", 7)

nescafe = Coffee.new("Nescafe", 1)

Now, for starters, lets just add this to an array and try and sort it.#

my_favorite_coffee = [laos, angkor, nescafe]

puts my_favorite_coffee.sort

This will blow up with the error in ‘sort’: comparison of Coffee with Coffee failed. This is becuase the Enumerable sort method (being called on the Array instance) does not have any idea how to sort a collection of Coffee objects. In order to fix this we need to implement the spaceship operator method on the Coffee class.

The Ruby Spaceship Operator

The Ruby spaceship operator is used to compare two objects. It returns only three possible values: -1, 0, 1. It works as follows

  • Returns -1 when the left side is LESS than the right side
  • Returns 0 when the left side is the SAME as the right side
  • Returns 1 when the left side is GREATER than the right side

This simple logic is then used by the Enumerable Modules sort method.

So without further ado lets implement our own spaceship operator method for our Coffee class. In our specific case we need to sort on the strength attribute.

def <=>(other_coffee)

  self.strength <=> other_coffee.strength

end

Now if we run the program again it does not fail but it does not work so well becuase we see three Coffee object strings like this output: <Coffee:0x007f9ada820f10>. What we really want is the human readable detail about the coffee name and strength. In order to do this we need to override the to_s method on the Coffee class.

Once we override the to_s method like so

def to_s

  "<name: #{name}, strength: #{strength}>"

end

Then our output is now as follows, which is much better!

<name: Nescafe, strength: 1>

<name: Angkor, strength: 7>

<name: Laos, strength: 10>

Enter the Coffee Shop!

So now that we know we have provided enough detail to sort our Coffee objects by strength within a generic Array, we need to be able to do the same within our CoffeeShop class too. This is achieved by implementing the Enumerable module in the CoffeeShop class. There are two things we need to do in our CoffeeShop class to achive this:

  1. Include the Enumerable module
  2. Implement an each method that yields each Coffee instance to the caller

Here is our completed CoffeeShop class:

class CoffeeShop

  include Enumerable

  attr_accessor :coffees



  def initialize(*coffees)

    @coffees = coffees

  end



  def each

    @coffees.map{|coffee| yield coffee}

  end

end

We can use it as follows and we get the same result as we do with calling sort on the Array.

laos = Coffee.new("Laos", 10) angkor = Coffee.new("Angkor", 7) nescafe = Coffee.new("Nescafe", 1) cs = CoffeeShop.new(nescafe, laos, angkor) puts cs.sort

Conculusion

So that is the basics of implementing Enumerable in Ruby. Here is a Gist that contains the full source code.

Contact
about Rotati

Rotati is a full service Web and Mobile application consultancy. We use Ruby and JavaScript as our tools of choice.