Chaining Enumerable Methods

Published by Programming Line in Ruby. Last updated .

Chaining is the practice of calling one Enumerable method right after another, letting the output of one step become the input of the next. Instead of writing a loop with several intermediate variables, you describe a pipeline: filter this, transform that, then total it up. Ruby is built for this style, and once you can read a chain fluently, entire loops disappear into a single, expressive line.

Overview: How Chaining Works

Enumerable is a module mixed into Array, Hash, Range, and any custom class that defines an each method and includes the module itself. Nearly every method you know — map, select, reject, sort_by, reduce, tally, group_by — is defined once in Enumerable and inherited by all of them. This is why an Array and a Range share the exact same vocabulary.

Chaining works because most Enumerable methods follow a simple contract: given a collection, they return a new collection (or a single value), without modifying the original. numbers.select { ... } does not touch numbers; it builds and returns a brand-new array. Because the return value is itself an object that responds to Enumerable methods, you can immediately call another method on it, and another, and another. Each link in the chain hands a fresh object to the next.

There is an important truthiness rule buried inside every filtering method. select and reject decide whether to keep an element based on whether the block’s return value is truthy, and in Ruby only nil and false are falsy. That means numbers.select { |n| n } keeps every number, including 0, because 0 is truthy in Ruby — unlike languages such as JavaScript, Python, or C. Always write an explicit condition (n.positive?, n != 0) rather than relying on a bare value being “falsy” the way it might be elsewhere.

Chains are normally eager: each method fully iterates its input and builds its complete output before the next method runs at all. That is fine for small, finite collections, but it becomes a problem for huge datasets or infinite sequences like (1..Float::INFINITY). Ruby solves this with lazy enumerators. Calling .lazy on a collection returns an Enumerator::Lazy that records the chain of operations without running any of them. Nothing actually executes until a terminal method — first, take, to_a, or force — pulls values through the whole pipeline one element at a time. This is what makes it safe to chain select and map onto an infinite range, something that would hang forever without .lazy.

Syntax

A chain is just a sequence of method calls, each one invoked on the return value of the one before it. When a chain grows past two or three links, the idiomatic style breaks it across lines with the dot leading each new line:

collection
  .method_one { |element| }
  .method_two { |element| }
  .method_three
Method Returns Purpose
map / collect Array Transform every element into something new
select / filter Array Keep elements where the block is truthy
reject Array Drop elements where the block is truthy
sort_by Array Order elements by a computed key
reduce / inject varies Fold all elements into one accumulated value
tally Hash Count how many times each element occurs
group_by Hash Partition elements into buckets by a key
flat_map Array map followed by flattening one level
first(n) / take(n) Array Terminal: stop after n elements
lazy Enumerator::Lazy Defer every step until a terminal call runs

Examples

Example 1: A basic filter-transform-reduce chain

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

result = numbers
  .select { |n| n.even? }
  .map { |n| n**2 }
  .sum

puts result

Output:

220

Ruby evaluates this strictly left to right. select walks all ten numbers and returns a new array of the even ones: [2, 4, 6, 8, 10]. That array becomes the receiver for map, which squares each value into [4, 16, 36, 64, 100]. Finally sum folds that array down to a single integer, 220, which gets assigned to result.

Example 2: Filtering and shaping a list of records

people = [
  { name: "Ava", age: 28 },
  { name: "Ben", age: 17 },
  { name: "Cleo", age: 34 },
  { name: "Drew", age: 22 }
]

adult_names = people
  .select { |person| person[:age] >= 18 }
  .sort_by { |person| person[:age] }
  .map { |person| person[:name] }

puts adult_names.join(", ")

Output:

Drew, Ava, Cleo

This is the shape most real chains take: filter out records you don’t care about, order what’s left, then pull out just the field you need. select drops Ben (age 17), sort_by arranges the remaining three youngest-first, and the final map reduces each hash down to a plain name string. Each step is a small, obvious idea, and reading top to bottom tells the whole story.

Example 3: Counting and ranking with tally

text = "the quick brown fox jumps over the lazy dog the fox runs the fox the dog"
stop_words = %w[the over]

top_words = text
  .split
  .map(&:downcase)
  .reject { |word| stop_words.include?(word) }
  .tally
  .sort_by { |_word, count| -count }
  .first(2)

top_words.each do |word, count|
  puts "#{word}: #{count}"
end

Output:

fox: 3
dog: 2

split breaks the sentence into individual words, reject strips out the stop words, and tally (a core Enumerable method) turns the remaining word list into a Hash of word to occurrence count. sort_by then orders that hash’s pairs by negative count so the most frequent word sorts first, and first(2) keeps only the top two. Notice a chain can freely switch between arrays and hashes — Hash includes Enumerable too, so sort_by works on it exactly the same way it works on an array.

Example 4: Lazy chaining over an infinite range

result = (1..Float::INFINITY).lazy
  .select { |n| n.odd? }
  .map { |n| n * n }
  .first(3)

puts result.inspect

Output:

[1, 9, 25]

(1..Float::INFINITY) never ends, so calling .select or .map on it directly would loop forever trying to build a complete intermediate array. Calling .lazy first changes the behavior entirely: select and map just record what should happen, and nothing runs until first(3) asks for three results. Ruby then pulls values one at a time — test if odd, square it, check the next — stopping the instant it has three answers. This pattern is essential any time your source is huge, unbounded, or expensive to fully materialize (a file being read line by line, for instance).

How It Works Step by Step

Walking through Example 1 again, in order:

  • Ruby evaluates numbers.select { |n| n.even? } completely: it iterates over all ten elements, runs the block for each one, and returns a new array holding only the ones that passed — [2, 4, 6, 8, 10]. The original numbers array is untouched.
  • That new array is the receiver for .map { |n| n**2 }. Ruby iterates over its five elements and returns yet another new array, [4, 16, 36, 64, 100].
  • That array becomes the receiver for .sum, which folds all five values together with + and returns a single Integer, 220.
  • result is assigned 220, and puts prints it.

Each step fully finishes — and allocates a whole new array — before the next one begins. That is eager evaluation. Example 4’s .lazy chain replaces this three-pass, three-array process with a single pass that pulls one value at a time through every step before moving to the next source element, which is the whole reason it can work on an infinite sequence at all.

Common Mistakes

Mistake 1: A bang method silently mutates the original collection

It’s tempting to drop a bang method into the middle of a chain without thinking about it, but sort! doesn’t just return a sorted array — it reorders the receiver in place, permanently.

original = [3, 1, 2]
sorted_doubled = original.sort!.map { |n| n * 2 }

puts sorted_doubled.inspect
puts original.inspect

Output:

[2, 4, 6]
[1, 2, 3]

The intent was to compute a new, sorted-and-doubled array, but original itself got permanently reordered as a side effect — a classic bug when original is shared with, or referenced by, other code. Using the non-mutating sort fixes it:

original = [3, 1, 2]
sorted_doubled = original.sort.map { |n| n * 2 }

puts sorted_doubled.inspect
puts original.inspect

Output:

[2, 4, 6]
[3, 1, 2]

original is left exactly as it was, because sort (no !) always returns a brand-new array.

Mistake 2: Chaining each where map was needed

each is built for running side effects, not for transforming data — it always returns its original, unmodified receiver, no matter what the block does.

numbers = [1, 2, 3]
result = numbers.each { |n| n * 10 }.select { |n| n > 5 }

puts result.inspect

Output:

[]

The block’s n * 10 values are computed and immediately thrown away; each hands select the original [1, 2, 3], and none of those are greater than 5, so the result is empty. The fix is to use map, which actually returns the transformed values:

numbers = [1, 2, 3]
result = numbers.map { |n| n * 10 }.select { |n| n > 5 }

puts result.inspect

Output:

[10, 20, 30]

As a rule of thumb: reach for each only at the very end of a chain (or not at all), and use map the moment you need the block’s return values to flow to the next step.

Best Practices

  • Once a chain has more than two or three links, format it with one method per line and a leading dot, so it reads top to bottom and diffs cleanly.
  • If a chain is doing more than one distinct idea, split it into a well-named intermediate variable or a small private method rather than cramming everything into one expression.
  • Know which links in your chain end in !; a single bang method mutates the original receiver and can silently break code elsewhere that still holds a reference to it.
  • Temporarily insert .tap { |x| p x } anywhere in a chain to inspect an intermediate value while debugging, then remove it once you’re done.
  • Reach for .lazy when the source is very large, unbounded (like an endless Range), or read incrementally from IO, so you never build a full intermediate array you don’t need.
  • Remember that many Enumerable methods called without a block return an Enumerator, which is itself chainable — you can compose iteration steps before ever supplying a block.
  • Prefer reduce or each_with_object over a long map/select chain when you’re really building up one accumulated value, since it avoids allocating a fresh array at every step.

Practice Exercises

  • Given numbers = (1..30).to_a, chain select, map, and sum to find the sum of the cubes of every multiple of 3 between 1 and 30. Hint: the expected result is 81675.
  • Given words = ["ruby", "elixir", "go", "elixir", "python", "go", "go"], chain tally, sort_by (or max_by), and a way to pull out just the key, to find the single most frequently occurring word. Hint: the answer is "go".
  • Using (1..Float::INFINITY).lazy, build a chain with select, map, and first(5) that finds the first five perfect squares greater than 100. Hint: the result should be [121, 144, 169, 196, 225].

Summary

  • Most Enumerable methods return a brand-new collection or value without touching the original, which is exactly what makes chaining them together possible.
  • A chain runs left to right, and by default each link fully finishes — building a complete intermediate array — before the next one starts.
  • Only nil and false are falsy in Ruby, so select/reject keep values like 0, "", and [] unless your condition says otherwise.
  • .lazy defers an entire chain into a single pass that only runs when a terminal method like first or to_a pulls results — essential for infinite or huge sources.
  • Bang methods like sort! mutate the receiver in place; mixing one into a chain changes the original object, not just the chain’s output.
  • each always returns its original receiver, never the block’s results — use map whenever the next link needs transformed values.
  • Break long or non-obvious chains across multiple lines, or extract them into a named method, to keep them readable for the next person (often future you).