← Back to blog

Enumerable: The Module That Runs Ruby

· Lachlan Young

If you’ve written Ruby for more than a week, you’ve used Enumerable. map, select, reject, reduce, any?, none?, flat_map, group_by, sort_by, each_with_object, these all come from the same place. One module, mixed into Array, Hash, Range, Set, and dozens of other classes, providing over fifty methods that transform how you work with collections.

Most developers learn the greatest hits and stop there. But understanding Enumerable as a design pattern, not just a bag of utility methods, changes how you write Ruby.

The contract

Enumerable asks almost nothing of you. Include the module and define each. That’s it.

class Playlist
  include Enumerable

  def initialize
    @tracks = []
  end

  def add(track)
    @tracks << track
  end

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

Your Playlist now responds to map, select, min_by, count, flat_map, zip, take_while, and everything else Enumerable provides. You wrote one method and got fifty.

This is Ruby’s duck typing philosophy applied to collections. Enumerable doesn’t care what your class stores or how it stores it. It just needs to iterate.

The methods you’re underusing

Every Ruby developer knows map and select. Here are the ones that tend to be overlooked.

each_with_object

Building a hash from an array? Stop reaching for reduce with a manual accumulator.

# The reduce way, works but awkward
users.reduce({}) do |hash, user|
  hash[user.id] = user.name
  hash  # easy to forget this
end

# The each_with_object way, cleaner
users.each_with_object({}) do |user, hash|
  hash[user.id] = user.name
end

The difference is subtle but real: each_with_object passes the accumulator as the second block argument, so you never need to return it explicitly. One fewer source of bugs.

tally

Counting occurrences? You don’t need group_by followed by transform_values(&:count).

%w[ruby python ruby go ruby python].tally
# => {"ruby"=>3, "python"=>2, "go"=>1}

One method, one line, exactly what you meant.

filter_map

Selecting and transforming in one pass instead of chaining select then map, or map then compact.

# Before
users.select(&:active?).map(&:email)

# Or
users.map { |u| u.email if u.active? }.compact

# After
users.filter_map { |u| u.email if u.active? }

It’s faster (one iteration instead of two) and communicates intent more directly: “give me the transformed values, but only the ones that exist.”

chunk and chunk_while

Grouping consecutive elements by some criteria. Useful for time-series data, log processing, or any sequence where adjacency matters.

temperatures = [22, 23, 25, 18, 17, 19, 24, 26]

temperatures.chunk { |t| t >= 20 ? :warm : :cool }.to_a
# => [[:warm, [22, 23, 25]], [:cool, [18, 17, 19]], [:warm, [24, 26]]]

group_by would lose the ordering. chunk preserves it.

each_cons and each_slice

Sliding windows and fixed batches over a collection.

# Sliding window of 3
[1, 2, 3, 4, 5].each_cons(3).to_a
# => [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

# Fixed batches of 2
[1, 2, 3, 4, 5].each_slice(2).to_a
# => [[1, 2], [3, 4], [5]]

These are common in data processing but rarely seen in application code, usually because developers don’t know they exist.

Lazy enumeration

Every Enumerable method creates a new array. Chain three together and you’ve iterated three times and allocated three arrays. For small collections, that’s fine. For large ones, it’s waste.

Lazy evaluates the chain on demand, one element at a time:

# Eager, reads entire file, creates two intermediate arrays
File.readlines("huge.log")
    .select { |line| line.include?("ERROR") }
    .map { |line| line.split(": ").last }
    .first(10)

# Lazy, stops after finding 10 matches
File.readlines("huge.log")
    .lazy
    .select { |line| line.include?("ERROR") }
    .map { |line| line.split(": ").last }
    .first(10)

The lazy version doesn’t process a single line beyond what’s needed to produce 10 results. Same code, same readability, dramatically different performance characteristics.

Building pipelines

Enumerable’s real power is chaining. Each method returns an Enumerable, so you can build data transformation pipelines that read top to bottom:

orders
  .select { |o| o.status == "completed" }
  .reject { |o| o.total.zero? }
  .group_by(&:customer_id)
  .transform_values { |orders| orders.sum(&:total) }
  .sort_by { |_id, total| -total }
  .first(10)
  .to_h

No intermediate variables. No temporary arrays with meaningless names. The data flows through a series of transformations, each one self-documenting. Read it from top to bottom and you know exactly what it does: completed non-zero orders, grouped by customer, summed, top 10 by revenue.

This is where Ruby’s readability philosophy meets practical data manipulation. The pipeline reads like a description of what you want, not instructions for how to compute it.

The hash side

Hash includes Enumerable too, which means every method works on key-value pairs. This is easy to forget.

prices = { apple: 1.20, banana: 0.50, cherry: 3.00, date: 8.50 }

# Find expensive items
prices.select { |_fruit, price| price > 2.0 }
# => {:cherry=>3.0, :date=>8.5}

# Get the cheapest
prices.min_by { |_fruit, price| price }
# => [:banana, 0.5]

each_with_object, flat_map, reduce, they all work on hashes. The block just receives [key, value] pairs. Once you internalise this, you stop converting hashes to arrays and back.

Enumerable and testing

Heavy use of Enumerable means your code produces and consumes hashes and arrays constantly. When tests fail, you’re often comparing complex nested structures, and the diff between expected and actual can be dense.

That’s the exact scenario RubyHash was built for. Paste your Minitest hash diff, and you get a sorted, highlighted comparison instead of squinting at two walls of text in your terminal. The more your code leans on Enumerable transformations, the more useful a clean diff becomes.

The design lesson

Enumerable is a masterclass in interface design. It asks for the minimum possible contract (define each), delivers maximum value (50+ methods), and composes cleanly (every method returns something Enumerable). It’s been stable for decades without breaking changes.

When you’re designing your own abstractions, that’s the bar. Small contract, big payoff, composable output. Most of the time you won’t hit it, but knowing what good looks like helps you recognise when you’re overcomplicating things.

The next time you reach for a while loop or a manual accumulator, pause. There’s almost certainly an Enumerable method that says what you mean more clearly. And if you’re not sure which one, ri Enumerable in your terminal will list them all.


Comparing hashes from an Enumerable pipeline gone wrong? Try RubyHash, paste your expected and actual output for an instant, readable diff.

Enjoyed this post?

Subscribe to get notified when we publish more Ruby and Rails content.