bcobb

Better Know Enumerable: Introduction

Sep 17, 2014

Ruby’s Enumerable module is the backbone that gives Array, Hash, IO, and a handful of other core classes methods like each and sort_by. More than basic traversal and sorting, it enables a mental and practical shift from imperative to functional programming. For instance, we could use each and a few conditionals to figure out which top-level classes include Enumerable:

enumerables = []
constant_names = Object.constants

constant_names.each do |constant_name|
  constant = Object.const_get(constant_name)

  if constant.respond_to?(:ancestors)
    if constant.ancestors.include?(Enumerable)
      enumerables << constant
    end
  end
end

However, using the map and select methods provided by Enumerable break down the problem in a more direct way and without local mutable state.

enumerables = Object.constants.
  map { |c| Object.const_get(c) }.
  select { |c| c.respond_to?(:ancestors) }.
  select { |c| c.ancestors.include?(Enumerable) }

An in-depth exploration of the rest of Enumerable could fill a small book, and this post will not attempt to fill in all the gaps. Instead, it provides a few examples using common and uncommon parts of Enumerable’s interface, and hopefully motivates further experimentation with the rest of the module.


Despite being used in the proverbially grainy and greyscale example above, each is the linchpin of Enumerable. In order for a class to include Enumerable it must implement each. For example, SentenceSearch below will traverse through all sentences which contain some word in a given body (for a naïve definition of “sentence”). We’ll use it to find the word “pudding” in an excerpt from Dickens’ Great Expectations (text copied from Project Gutenberg).

class SentenceSearch
  include Enumerable

  def initialize(body, word)
    @body = body
    @word = word
  end

  def each
    @body.
      split('.').
      select { |sentence| sentence.include?(@word) }.
      each { |sentence| yield "#{sentence.lstrip}." }
  end
end

excerpt = <<-EXCERPT
  By degrees, I became calm enough to release my grasp
  and partake of pudding. Mr. Pumblechook partook of
  pudding. All partook of pudding. The course terminated,
  and Mr. Pumblechook had begun to beam under the genial
  influence of gin and water. I began to think I should
  get over the day, when my sister said to Joe,
  "Clean plates,--cold."
EXCERPT
usable_excerpt = excerpt.lines.map(&:strip).join(' ')

puddings = SentenceSearch.new(usable_excerpt, 'pudding')

Being able to traverse through these sentences is useful—many search engines provide this sort of view when displaying search results—but Enumerable facilitates working with these collections of sentences in a surprising number of ways.

We can find the shortest and longest sentence:

pp puddings.minmax_by { |sentence| sentence.length }

# ["All partook of pudding.",
#  "By degrees, I became calm enough to release my grasp and partake of pudding."]

Or just the shortest sentence:

pp puddings.min_by { |sentence| sentence.length }

# "All partook of pudding."

Using the union operation on Array |, we can compile the collection of unique words in all of the sentences:

puddings.
  map { |sentence| sentence.split(' ') }.
  reduce { |c, words| c | words }

# ["By",
#  "degrees,",
#  "I",
#  "became",
#  "calm",
#  "enough",
#  "to",
#  "release",
#  "my",
#  "grasp",
#  "and",
#  "partake",
#  "of",
#  "pudding.",
#  "Pumblechook",
#  "partook",
#  "All"]

Enumerable is a generic module for collection traversal; with a little imagination we could conjure up dozens of additional ways to process the base collection of pudding-related sentences. In future posts, I’ll cover Enumerable’s methods in greater depth, and show real-world examples of refactoring towards an Enumerable class to reap the benefits of its familiar and powerful interface.