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
mapto transform each item in a collection. - Use
selectandrejectto keep or discard matching items. - Use
find,any?,all?, andnone?to answer questions about the data. - Use
reduce,sum, andtallywhen you need an aggregate result. - Use
lazywhen 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
mapandflat_mapto convert collections. - Filter data: Use
select,reject, andfindto extract specific elements. - Aggregate data: Use
reduce,sum, andcountto compute totals. - Search: Use
any?,all?,none?, andone?to check conditions. - Group: Use
group_by,tally, andzipto 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.
Forward link
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
- Enumerable in the Ruby docs — Official API reference
Array#map— The array method that most often pairs with EnumerableEnumerable#select— Filter collections with a blockEnumerable#reduce— Accumulate values into one result