rubyguides

Ruby Enumerable: map, filter, and aggregate collections

The Ruby Enumerable module is one of the most useful parts of the language. It gives you a consistent set of methods for iterating, searching, transforming, and aggregating collections. Any class that implements each can include Enumerable and immediately gain methods like map, select, reduce, group_by, and many others.

Intro context

Enumerable is the part of Ruby that turns “I have a collection” into “I can ask useful questions about this collection.” Once a class exposes each, you do not need to write a custom loop for every task. Instead, you can express the intent directly: transform each value, keep the matching ones, count the results, or stop at the first useful item.

That matters because collection code tends to repeat itself. Enumerable gives that repetition a standard shape, which makes Ruby code shorter and easier to scan. It also helps different collection types behave the same way, so arrays, hashes, ranges, and your own classes can all share the same methods.

TL;DR

  • Use map to transform each item in a collection.
  • Use select and reject to keep or discard matching items.
  • Use find, any?, all?, and none? to answer questions about the data.
  • Use reduce, sum, and tally when you need an aggregate result.
  • Use lazy when a collection is large or may be infinite.

What is Enumerable?

Enumerable is a mixin module that provides traversal, searching, grouping, and sorting behavior. To use it, your class must define an each method that yields each element in turn. Once that exists, Enumerable can build the rest of the interface on top of it.

Ruby includes Enumerable in core classes like Array, Hash, Range, and Set:

# Arrays include Enumerable
[1, 2, 3].map { |n| n * 2 }  # => [2, 4, 6]

# Hashes include Enumerable
{ a: 1, b: 2 }.select { |_k, v| v > 1 }  # => { b: 2 }

# Ranges include Enumerable
(1..5).sum  # => 15

The key idea is that Enumerable does not care what the collection stores. It cares that the collection can yield values one at a time. That makes it a very small abstraction with a lot of practical value.

Transforming with map and flat_map

The map method, also known as collect, transforms each element in a collection and returns a new array with the results. It is one of the most common Enumerable methods because it matches the way people think about a “do something to every item” operation.

numbers = [1, 2, 3, 4, 5]

# Square each number
squared = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]

# Convert to strings
stringified = numbers.map(&:to_s)
# => ["1", "2", "3", "4", "5"]

The &:to_s syntax is Ruby’s shorthand for passing a method name as a block. It calls to_s on each element, which is equivalent to { |n| n.to_s }. This pattern works with any method that takes no arguments and is especially common with map, select, and reduce.

If your transformation returns arrays and you want to flatten the result, use flat_map. That keeps the code compact when the transformation naturally expands one item into many.

words = ["hello", "world"]

# map would return arrays of characters
chars = words.map { |word| word.chars }
# => [["h", "e", "l", "l", "o"], ["w", "o", "r", "l", "d"]]

# flat_map flattens one level
flat_chars = words.flat_map { |word| word.chars }
# => ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]

When you are deciding between map and flat_map, ask whether you want an array of arrays or one flattened result. flat_map is equivalent to map{...}.flatten(1) but does the work in one pass. That small question usually makes the right method choice obvious.

Filtering with select and reject

The select method, also known as find_all, returns the elements that match a condition. It is the method you reach for when the important part is keeping only the values that satisfy some rule.

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Get even numbers
evens = numbers.select { |n| n.even? }
# => [2, 4, 6, 8, 10]

# Get numbers greater than 5
greater_than_five = numbers.select { |n| n > 5 }
# => [6, 7, 8, 9, 10]

The opposite of select is reject, which returns elements that do not match. That is useful when you want to strip out a small class of values without building an explicit exclusion loop.

numbers = [1, 2, 3, 4, 5]

# Remove odd numbers
no_odds = numbers.reject { |n| n.odd? }
# => [2, 4]

You can also use filter_map to combine filtering and transformation in one pass. That is handy when you want to produce a compact result and the intermediate values are not useful on their own.

numbers = [1, 2, 3, 4, 5]

# Only double even numbers
result = numbers.filter_map { |n| n * 2 if n.even? }
# => [4, 8]

filter_map combines filtering and transformation into a single pass, which is more efficient than chaining select then map because it avoids creating an intermediate array. It also automatically skips nil results, so you can use a conditional expression directly in the block without a separate compact call.

Finding elements

The find method, also known as detect, returns the first element matching a condition. It is a good fit when you only care about the first match and want to stop searching as soon as you find it.

numbers = [1, 2, 3, 4, 5]

# Find first even number
first_even = numbers.find { |n| n.even? }
# => 2

# Find returns nil if no match
no_match = numbers.find { |n| n > 100 }
# => nil

Use find_all to get all matches, which is the same idea as select but sometimes reads more explicitly when the search behavior matters more than the filtering label.

numbers = [1, 2, 3, 4, 5]

all_evens = numbers.find_all { |n| n.even? }
# => [2, 4]

find_all is an alias for select — they do the same thing. Some Ruby developers prefer find_all when the search intent matters more than the filtering label, but the two are interchangeable. Use whichever reads more naturally in your codebase.

The any?, all?, none?, and one? methods let you check conditions across the whole collection. They are especially useful in validations and guard clauses because they give you a yes-or-no answer without needing to manage the loop yourself.

numbers = [1, 2, 3, 4, 5]

numbers.any? { |n| n > 3 }   # => true (at least one > 3)
numbers.all? { |n| n > 0 }    # => true (all are > 0)
numbers.none? { |n| n > 10 }  # => true (none are > 10)
numbers.one? { |n| n == 3 }   # => true (exactly one equals 3)

The predicate methods any?, all?, none?, and one? are especially useful in guard clauses and validations. They turn a collection-wide question into a boolean answer without requiring a manual loop or counter variable. If the block is omitted, they check truthiness of each element directly.

Aggregating with reduce

The reduce method, also known as inject, accumulates values across a collection. If you need to turn many items into one result, reduce is the most flexible building block.

numbers = [1, 2, 3, 4, 5]

# Sum all numbers
sum = numbers.reduce(0) { |acc, n| acc + n }
# => 15

# Or use the shorthand with :+
sum = numbers.reduce(:+)
# => 15

# Multiply all numbers
product = numbers.reduce(1) { |acc, n| acc * n }
# => 120

You can also use sum directly for simpler cases. That keeps the code easier to read when you are not doing anything custom with the accumulator.

numbers = [1, 2, 3, 4, 5]
numbers.sum        # => 15
numbers.sum(100)   # => 115 (starts with 100)

sum with an initial value is handy for running totals or when you want to offset the result. For more complex aggregations, reduce gives you full control over the accumulator and the combining logic. The shorthand reduce(:+) uses Ruby’s symbol-to-proc conversion to call the + method on each pair of values.

Grouping and counting

Group elements by a criterion using group_by. The result is a hash keyed by the value returned from the block, which makes it easy to spot clusters or buckets in the data.

words = ["apple", "banana", "cherry", "date"]

by_length = words.group_by { |word| word.length }
# => {5=>["apple"], 6=>["banana", "cherry"], 4=>["date"]}

Count occurrences with tally when you want a frequency table. That is the compact choice for repeated values because it handles the counting logic for you.

letters = ["a", "b", "a", "c", "b", "a", "d"]

counts = letters.tally
# => {"a"=>3, "b"=>2, "c"=>1, "d"=>1}

tally is one of the newer Enumerable methods (Ruby 2.7+) and it replaces a common each_with_object pattern for counting frequencies. It returns a hash where each unique value maps to its count, making it ideal for histograms, word frequency analysis, and any situation where you need to know how often each value appears.

Sorting

The sort_by method sorts elements by a computed value. It is usually easier to read than building your own comparator because the block describes the sorting key directly.

words = ["apple", "banana", "cherry", "date"]

# Sort by length
by_length = words.sort_by { |word| word.length }
# => ["date", "apple", "banana", "cherry"]

# Sort by multiple criteria
words.sort_by { |w| [w.length, w] }
# => ["date", "apple", "banana", "cherry"]

For descending order, chain reverse. That is simple and readable when you already have the right sort order and only need to flip it.

numbers = [3, 1, 4, 1, 5, 9, 2, 6]

numbers.sort.reverse
# => [9, 6, 5, 4, 3, 2, 1, 1]

For descending order, sort.reverse is the simplest approach, but sort_by { |n| -n } also works and avoids creating an intermediate array. Choose whichever reads more naturally in context — the performance difference is negligible for most collections.

Combining collections

The zip method combines elements from multiple arrays. It is a tidy way to pair related values when the positions already line up.

names = ["alice", "bob", "charlie"]
ages = [25, 30, 35]

zipped = names.zip(ages)
# => [["alice", 25], ["bob", 30], ["charlie", 35]]

# Convert to hash
names.zip(ages).to_h
# => {"alice"=>25, "bob"=>30, "charlie"=>35}

That pattern is useful when you start with a list of keys and a list of values and want a hash without manually building it entry by entry. zip pairs elements by position, and to_h converts those pairs into key-value entries. This is a common shortcut when data arrives in parallel arrays from CSV parsing or API responses.

Lazy enumeration

For large or infinite collections, use lazy to defer evaluation. Lazy enumeration is a good fit when you do not need the entire result at once and want to avoid building intermediate arrays.

# This would hang without lazy!
result = (1..Float::INFINITY)
  .lazy
  .select { |n| n.even? }
  .first(5)

# => [2, 4, 6, 8, 10]

Lazy enumeration is powerful for processing large datasets or working with streams without loading everything into memory. Without lazy, chaining select then first on an infinite range would hang forever because Ruby would try to filter the entire collection before taking the first five. The lazy call defers evaluation so each step only processes what it needs.

Enumerable with custom objects

You can add Enumerable to your own classes by implementing each. Once each exists, all of the familiar methods become available without any extra work.

class Inventory
  include Enumerable

  def initialize
    @items = []
  end

  def add(item)
    @items << item
    self
  end

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

inventory = Inventory.new
inventory.add("apple").add("banana").add("cherry")

# Now you can use Enumerable methods
inventory.map(&:upcase)
# => ["APPLE", "BANANA", "CHERRY"]

inventory.select { |item| item.start_with?("a") }
# => ["apple"]

That pattern is one of the best reasons to understand Enumerable well: it lets your own classes feel like native Ruby collections. By implementing just one method, you gain access to dozens of iteration, transformation, and query methods without writing a single loop.

When to use enumerable

Enumerable methods work best when you need to:

  • Transform data: Use map and flat_map to convert collections.
  • Filter data: Use select, reject, and find to extract specific elements.
  • Aggregate data: Use reduce, sum, and count to compute totals.
  • Search: Use any?, all?, none?, and one? to check conditions.
  • Group: Use group_by, tally, and zip to organize related values.

Avoid using Enumerable methods when you need an imperative loop with an early exit. In those cases, a simple each with break can be more direct.

Frequently asked questions

When should I choose map instead of each?

Choose map when you need a new collection back. Use each when you only want to perform side effects, such as printing or updating another object.

Is reduce only for sums?

No. reduce is for any accumulation pattern, including concatenation, product, merging, or building a hash from a list of values.

Why does lazy matter?

lazy lets Ruby postpone work until it actually needs the next value. That is valuable for big collections, infinite ranges, or pipelines where you only need the first few results.

Once you are comfortable with Enumerable, the next step is usually to combine it with hashes, strings, and your own objects so collection code stays small but still reads clearly.

Summary

Ruby’s Enumerable module provides a rich set of methods for working with collections. By understanding these methods, you can write more expressive, concise, and Ruby-idiomatic code. The key methods to remember are:

  • Transformation: map, flat_map, filter_map
  • Filtering: select, reject, find
  • Aggregation: reduce, sum, count
  • Searching: any?, all?, none?, one?
  • Organization: group_by, tally, sort_by, zip

Practice using these methods in your Ruby code, and you’ll find yourself writing more elegant solutions to common programming problems.

See Also