Getting to know Ruby’s Enumerable Module

rmontas

Rafael Montás

Posted on April 16, 2023

Getting to know Ruby’s Enumerable Module

Enumerable is a module in the Ruby standard library. Classes that include it have to define an instance method called each, which yields the elements of the collection in succession. Once the iterator is defined and Enumerable is mixed in, the class now supports all sorts of collection-related behavior. Also, keep in mind, that although Enumerable adds common functionality to each of the collection classes that includes it, each of them actually overrides some of the methods.

class MyArray
  include Enumerable

  def each
    # yield elements of the collection
  end
end
Enter fullscreen mode Exit fullscreen mode

You can query which methods Enumerable provides by sending the messageinstance_methods to it and passing the false argument so you get only methods defined in the module itself.

irb(main):001:0> Enumerable.instance_methods(false)
Enter fullscreen mode Exit fullscreen mode

In order to understand Enumerable at a deeper level, let’s write a few of its methods while trying to cover as many operations as posible.

Methods for Querying

Some Enumerable methods return information about the collection other than the elements themselves. Some return either true or false if one or more element matches certain criteria.

#include?

Returns true if the item yielded to the block is equal the argument passed, falseotherwise.

def include?(value)
  self.each { |item| return true if item == value }
  return false
end

# (1..5).include?(2)
# => true
Enter fullscreen mode Exit fullscreen mode

This works just fine for Arrays but if we were querying a Hash, we would need to adjust for the fact that when you iterate through a Hash with each it yields back one key/value pair at a time (two-element arrays). The Hash#include? method checks for key inclusion

#any?

Returns true if any element meets a specified criteria, false otherwise.

def any?
  return self.length >= 1 unless block_given?

  self.each do |element|
    return true if yield(element)
  end

  return false
end

# => (1..5).any? { |n| n > 3 }
# => true
Enter fullscreen mode Exit fullscreen mode

#count

Returns the count of elements based on a criteria passed as argument or a block, if given.

def count(value=nil)
  counter = 0
  self.each do |element|
    if value
      counter += 1 if element == value
    elsif block_given?
      counter += 1 if yield(element)
    else
      counter += 1
    end
  end

  warn("warning: given block not used") if value && block_given?
  return counter
end

# (1..5).count { |value| value > 3 }
# => 2
Enter fullscreen mode Exit fullscreen mode

#tally

Counts the ocurrences of each element. Returning a Hash with the collection’s elements as keys and corresponding counts as values.

def tally
  tally = Hash.new(0)
  self.each { |element| tally[element] += 1 }

  return tally
end

# [1, 2, 2, 2, 3, 3, 4].tally
# => {1=>1, 2=>3, 3=>2, 4=>1}
Enter fullscreen mode Exit fullscreen mode

Methods for Searching and Filtering

It’s common to want to filter a collection of objects based on a selection criteria. We’ll look at several facilities for filtering and searching collections. All of them expect a code block, where you difine your selection criteria (your tests for inclusion or exclusion).

#find aliased as #detect

Returns the first element in the collection for which the code block returns true. If no code block is provided, it returns an instance of Enumerator.

def find
  return self.enum_for( __method__ ) unless block_given?
  self.each { |element| return element if yield(element) }

  return nil
end

alias :detect :find

# (1..5).find {|value| value > 2}
# => 3
Enter fullscreen mode Exit fullscreen mode

#find_all aliased as #select

Returns an Array containing all the elements of the original collection that matched the criteria in the code block. If no matching elements are found, it returns an empty collection. Also, if no code block is provided, it returns an instance of Enumerator.

def find_all
  return self.enum_for( __method__ ) unless block_given?

  matches = []
  self.each do |element|
    matches << element if yield(element)
  end

  return matches
end

alias :select :find_all

# (1..9).find_all {|value| value % 3 == 0 }
# => [3, 6, 9]
Enter fullscreen mode Exit fullscreen mode

#find_index

Returns the index of the first element that meets the specified criteria, or nil if no element matches the criteria. It returns an instance of Enumerator if neither a code block nor an argument are provided.

For this method to work, make sure your #each implementation returns anEnumerator when no code block is provided. This will allow us to chain methods, as in .each.with_index.

def find_index(value=nil)
  return self.enum_for( __method__ ) unless value || block_given?

  self.each.with_index do |element, index|
    if value
      return index if element == value
    else
      return index if yield(element)
    end
  end

  return nil
end

# (1..5).find_index(2)
# => 1
Enter fullscreen mode Exit fullscreen mode

#group_by

When given a code block, it returns a Hash. For each unique value returned by the block, the results hash gets a key; the value for that key is an Array of all the elements of the collection for which the block returned that value. If no code block is given, it returns an instance of Enumerator.

def group_by
  return self.enum_for( __method__ ) unless block_given?

  groups = Hash.new([])
  self.each do |element|
    groups[yield(element)] << element
  end

  return groups
end

# colors = [{group: 'primary'}, {group: 'secondary'}, {group: 'primary'}]
# colors.group_by { |value| value[:group] }
# => {"primary"=>[{:group=>"primary"}, {:group=>"primary"}], "secondary"=>[{:group=>"secondary"}]}
Enter fullscreen mode Exit fullscreen mode

#map aliased as #collect

Returns an Array when given a block. Returns and Enumerator otherwise. The returned array is always the same size as the original collection. Its elements are the result of calling the code block on each element in the original object.

def map
  return self.enum_for( __method__ ) unless block_given?

  collection = []
  self.each { |element| collection << yield(element) }

  return collection
end

alias :collect :map

# (1..5).map { |value| value * value }
# => [1, 4, 9, 16, 25]
Enter fullscreen mode Exit fullscreen mode

#inject aliased as #reduce

Works by initializing an accumulator object, performs a calculation on each iteration and reset the accumulator to the result of that calculation. Returns the return value from the last block call.

def inject(operand=0)
  accumulator = operand
  self.each do |element|
    accumulator = yield(accumulator, element)
  end

  return accumulator
end

alias :reduce :inject

# (1..5).inject { |sum, value| sum + value }
# => 15
Enter fullscreen mode Exit fullscreen mode

We’ve gotten to know Enumerable in a more detailed way and it will better positioned us to handle everyday programming as we’ll use the enumeration-related facilities of the language virtually every time we write a Ruby program.

💖 💪 🙅 🚩
rmontas
Rafael Montás

Posted on April 16, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Let's Read - Eloquent Ruby - Ch 17
ruby Let's Read - Eloquent Ruby - Ch 17

September 24, 2024

Ruby Reduce
ruby Ruby Reduce

January 24, 2022