Jul 1, 2026
The Enumerable Methods I Keep Coming Back To
Nine Enumerable methods beyond map and select that I actually use, grouped by intent. The ones that replaced loops in my code.
Everyone reaches for map, select, and reject on day one. The interesting Enumerable methods are the ones you discover on day 1,000 — and then can’t stop using. Here’s nine of those, grouped by what they’re for.
Counting and grouping
tally
Counts occurrences of each element. The shortest possible word frequency table:
# illustrative
%w[apple banana apple cherry apple banana].tally
# => { "apple" => 3, "banana" => 2, "cherry" => 1 }
Before tally was added, this was a six-line each_with_object(Hash.new(0)). Now it’s one method. Every time I write a “how many of each” query in Ruby instead of SQL, I reach for tally first.
group_by
Group elements by the result of a block:
# illustrative
jobs.group_by(&:remote_type)
# => { "remote" => [...], "hybrid" => [...], "onsite" => [...] }
tally is group_by(&:itself).transform_values(&:size). They’re cousins. Reach for group_by when you want the actual elements per bucket; reach for tally when you only want the counts.
Reducing
each_with_object
The accumulator-style fold. The argument order is (item, accumulator) and the accumulator is returned automatically:
# illustrative
salaries.each_with_object({ min: Float::INFINITY, max: 0 }) do |s, acc|
acc[:min] = s if s < acc[:min]
acc[:max] = s if s > acc[:max]
end
The difference from inject/reduce is that you don’t have to return the accumulator from the block. That’s the whole win — no acc; end at the bottom of every iteration.
sum (with block)
Sum a collection by some computed value:
# illustrative
orders.sum(&:total_cents)
# or
orders.sum { |o| o.total_cents * o.quantity }
There’s also a starting value: orders.sum(0) { ... }. Without the block, sum works on numerics directly. Replaces every inject(0) { |acc, x| acc + x.something } I used to write.
partition
Split into two arrays based on a predicate, in one pass:
# illustrative
recent, old = jobs.partition { |j| j.posted_at > 7.days.ago }
The alternative is two select calls, which iterates twice. partition is one pass and reads better.
Finding
min_by and max_by
Find the element that minimizes or maximizes a function:
# illustrative
cheapest = products.min_by(&:price)
newest = articles.max_by(&:published_at)
The variants min_by(n) and max_by(n) return the n smallest/largest as an array. That’s a top-N query without sorting the whole collection. For large datasets this matters.
find_index
Like find, but returns the index instead of the element:
# illustrative
chapters.find_index { |c| c.title.start_with?("Conclusion") }
# => 7
Pair with take(n) or drop(n) to slice a collection at a logical boundary. Underrated for parsing serialized data.
Slicing into runs
chunk_while and slice_when
These are sister methods. chunk_while keeps elements in the same chunk while the predicate is true between consecutive pairs; slice_when starts a new slice when the predicate is true. They’re the same operation with inverted logic.
# illustrative
# Group consecutive integers
[1, 2, 3, 5, 6, 8, 9, 10].chunk_while { |a, b| b - a == 1 }.to_a
# => [[1, 2, 3], [5, 6], [8, 9, 10]]
# Same thing with slice_when
[1, 2, 3, 5, 6, 8, 9, 10].slice_when { |a, b| b - a > 1 }.to_a
# => [[1, 2, 3], [5, 6], [8, 9, 10]]
I use chunk_while for grouping log entries by time gap, splitting paginated results by date, and bucketing sorted records into runs. The block sees consecutive pairs, which is a different mental model than most Enumerable methods — but it unlocks a class of problems that are awkward without it.
Performance
lazy
Defers iteration. Useful when chaining transformations on a large (or infinite) collection where you only need a few results:
# illustrative
(1..Float::INFINITY).lazy.map { |n| n ** 2 }.select(&:odd?).first(10)
# => [1, 9, 25, 49, 81, 121, 169, 225, 289, 361]
Without lazy, the map would try to materialize an infinite array and hang forever. With it, only 10 elements are pulled through the chain.
When to use lazy on a finite collection: when the chain has multiple transformations and you’ll stop early. For a 10,000-element array where you .lazy.map.select.first(5), you process roughly the first dozen elements, not all 10,000. For full-collection processing, plain Enumerable is faster (no overhead) — lazy only wins when you stop early.
Quick reference
| Intent | Method | Notes |
|---|---|---|
| Count occurrences | tally | Returns Hash of counts |
| Group by key | group_by | Returns Hash of arrays |
| Build with accumulator | each_with_object | (item, acc) order |
| Sum by computed value | sum(&:method) | Block optional |
| Split by predicate | partition | Returns [truthy, falsy] |
| Smallest/largest | min_by / max_by | min_by(3) for top-N |
| Find by predicate | find_index { ... } | Returns Integer or nil |
| Group consecutive runs | chunk_while | Block sees pairs |
| Same, inverted | slice_when | Block sees pairs |
| Defer iteration | lazy | Stop-early chains |
The thing about Enumerable: every method on it works on every class that includes Enumerable. That’s Array, Hash, Range, Set, ActiveRecord relations (effectively), file lines, and anything you implement each on. Learning these once gives you back ten years of code-writing time.