100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Ruby

Enumerable Deep Dive

Go beyond map and select to explore Enumerable's full toolkit — sort_by, group_by, partition, each_with_object, lazy enumerators, and more — for expressive collection processing.

Iterators & EnumerableAdvanced11 min readJul 9, 2026
Analogies

Enumerable Deep Dive

Enumerable is one of Ruby's richest modules, offering dozens of methods beyond the well-known each, map, and select. Mastering the fuller toolkit — sort_by, group_by, partition, each_with_object, tally, flat_map, zip, and lazy enumerators — lets you express complex data transformations in a single expressive chain instead of nested loops and mutable accumulator variables. All of these methods are available to any class that mixes in Enumerable and defines each, as well as to Array, Hash, Range, and Set out of the box.

🏏

Cricket analogy: Beyond just watching every ball (each) or scoring runs per over (map), a stats analyst reaches for sort_by to rank batsmen, group_by to bucket by team, and partition to split centuries from non-centuries in one expressive chain.

sort_by, group_by, and partition

sort_by sorts using a computed key rather than the elements' natural comparison, and is generally faster than sort with a block because it computes the key once per element (a Schwartzian transform) rather than recomputing it on every comparison. group_by buckets elements into a Hash keyed by the block's return value. partition splits a collection into exactly two arrays — elements where the block is truthy, and where it's falsy.

🏏

Cricket analogy: Ranking bowlers by economy rate computed once per bowler (sort_by) is faster than recomputing it on every comparison; group_by buckets batsmen into a Hash by their team, and partition splits the innings into boundary-scoring shots versus dot balls.

ruby
employees = [
  { name: 'Ada', dept: 'Engineering', salary: 95_000 },
  { name: 'Grace', dept: 'Engineering', salary: 110_000 },
  { name: 'Alan', dept: 'Sales', salary: 70_000 }
]

by_salary = employees.sort_by { |e| -e[:salary] }
by_dept   = employees.group_by { |e| e[:dept] }
#=> { 'Engineering' => [Ada, Grace], 'Sales' => [Alan] }

high_earners, others = employees.partition { |e| e[:salary] > 90_000 }

each_with_object and tally

each_with_object is a cleaner alternative to reduce when the accumulator is a mutable object (like a Hash or Array) — it yields the accumulator alongside each element and always returns the accumulator automatically, eliminating the need to explicitly return it. tally counts occurrences of each unique element, returning a Hash of element to count — a shortcut that replaces a common each_with_object/reduce pattern.

🏏

Cricket analogy: Building a Hash of runs-per-batsman while iterating deliveries is cleaner with each_with_object than reduce, since it yields the accumulator alongside each ball; tally then counts how many times each bowler dismissed a batsman in one shortcut.

ruby
words = %w[apple banana apple cherry banana apple]

counts = words.each_with_object(Hash.new(0)) { |word, h| h[word] += 1 }
#=> {'apple'=>3, 'banana'=>2, 'cherry'=>1}

words.tally
#=> {'apple'=>3, 'banana'=>2, 'cherry'=>1}  (equivalent, built in since Ruby 2.7)

Lazy enumerators for large or infinite sequences

Chaining several Enumerable methods on a large collection builds an intermediate array at every step, which is wasteful. .lazy returns a lazy enumerator that defers evaluation, processing elements one at a time through the whole chain only as they're requested — essential for working with infinite sequences (like 1..Float::INFINITY) or avoiding unnecessary work when only a few results are needed.

🏏

Cricket analogy: Chaining filters and transforms over every ball ever bowled in Test history would build a huge intermediate array at each step; .lazy defers evaluation, useful when you only need the first five centuries matching a condition rather than scanning everything.

ruby
first_five_even_squares = (1..Float::INFINITY).lazy
  .map { |n| n ** 2 }
  .select(&:even?)
  .first(5)
#=> [4, 16, 36, 64, 100]

flat_map combines map and flatten(1) in one pass — useful when your block itself returns an array per element and you want a single flattened result, such as mapping each sentence in an array to its array of words and getting back one flat array of all words.

sort_by { |e| -e[:salary] } works for numeric descending sort, but for non-numeric keys, use sort_by { ... }.reverse or the two-argument sort { |a, b| b <=> a } instead, since you can't negate a String or arbitrary object directly.

  • sort_by sorts by a computed key, generally faster than sort with a block for expensive comparisons.
  • group_by buckets elements into a Hash keyed by the block's result.
  • partition splits a collection into two arrays based on a truthy/falsy block.
  • each_with_object is cleaner than reduce for building up a mutable accumulator.
  • tally counts occurrences of unique elements without any block needed.
  • .lazy defers evaluation through a chain, essential for infinite or very large sequences.

Practice what you learned

Was this page helpful?

Topics covered

#Ruby#RubyStudyNotes#Programming#EnumerableDeepDive#Enumerable#Deep#Dive#Sort#StudyNotes#SkillVeris