Ruby: Provide ways to iterate over collections

Sometimes we have classes that represent collections, for example a class Group can represent a collection of members

class Group
  def initialize(members)
    @members = members.dup
  end
end

When we want to iterate over the collection’s members we have to expose them through attr_reader

class Group
  attr_reader :members
 
  def initialize(members)
    @members = members.dup
  end
end
 
group1 = Group.new(["Nick", "Ana", "Petros"])
 
group1.members.each { |x| p |x| }

The problem with this approach is that the consumers of our collection have to know that we named our variable “members” and use it to iterate. If we ever rename this variable, the consumers of our collection have to rename their call as well. Moreover, we can’t control the order of the enumeration. What can we do to fix that?

We can make our collection an Enumerable, and define an “each” method

class Group
  include Enumerable
 
  def initialize(members)
    @members = members.dup
  end
 
  def iterate_members_alphabeticaly
    ite = @members.dup.sort
    while k = ite.shift()
      yield k
    end
  end
 
  def iterate_members_by_name_length
    ite = @members.dup.sort_by { |x| x.size }
    while k = ite.shift()
      yield k
    end
  end
 
  def each(&block)
    enum_for(:iterate_members_by_name_length).each(&block)
  end
end
 
 
group1 = Group.new(["Nick", "Petros", "Ana"])
 
group1.each {|x| p x }

That way we don’t expose the name of the variable and we can control the order of the enumeration, e.g. in the previous example we can change the :iterate_members_by_name_length with :iterate_members_alphabeticaly

def each(&block)
  enum_for(:iterate_members_by_name_length).each(&block)
end
Published 5 Nov 2018

Engineering Manager. Opinions are my own and not necessarily the views of my employer.
Avraam Mavridis on Twitter