reduce, each_with_object, and Accumulator Design
Ruby source files are converted directly from the English source tree.
en/topic_15_reduce_accumulators
Overview
Why this topic matters ๐ก
Topic 14 taught transformation pipelines. Topic 15 teaches the next step: problems where you need to build one result from many inputs. This is where reduce and each_with_object become practical tools rather than abstract ideas.
Learning outcomes ๐ฏ
By the end of this topic, students should be able to:
- explain when
reducefits better thanmap; - use
each_with_objectto build hashes and grouped structures clearly; - compare manual mutable accumulation with accumulator-style Ruby;
- choose an accumulator shape that matches the reporting or grouping problem;
- explain why these patterns are common in production Ruby code.
Assessment focus โ
Students should be able to defend the accumulator they chose and explain why it is clearer than a manual loop with external mutable state.
Short Note
Not every collection problem is a map. Some problems ask for:
- one total;
- one grouped hash;
- one summary object;
- one combined result built step by step.
That is where accumulator thinking matters.
Imperative style often looks like this:
total = 0
rows.each do |row|
total += row[:amount]
endruby
Ruby accumulator style can look like this:
rows.reduce(0) { |sum, row| sum + row[:amount] }ruby
For hashes or grouped structures, each_with_object often reads even better:
rows.each_with_object({}) do |row, grouped|
grouped[row[:currency]] ||= 0
grouped[row[:currency]] += row[:amount]
endruby
Ruby beauty in this topic:
- the accumulator is visible in the method call;
- the code answers "what are we building?" directly;
- grouped results become much easier to read.
Ruby caution in this topic:
reducecan become cryptic if the accumulator shape is not obvious;- not every accumulator should be hidden in a clever one-liner;
- a named helper can still be the clearest choice.
Reflection prompt:
- Why is
each_with_objectoften easier to read thanreducefor hash-building tasks?
Worked Examples
Example 1: Invoice total ๐ก
Old style:
total = 0
invoices.each do |invoice|
total += invoice[:amount]
endruby
Accumulator style:
invoices.reduce(0) { |sum, invoice| sum + invoice[:amount] }ruby
Why the second version helps:
- the initial value is explicit;
- the result type is clear from the start;
- the loop variable does not escape its purpose.
Example 2: Group names by category ๐ก
Old style:
grouped = {}
items.each do |item|
grouped[item[:category]] ||= []
grouped[item[:category]] << item[:name]
endruby
Ruby style:
items.each_with_object({}) do |item, grouped|
grouped[item[:category]] ||= []
grouped[item[:category]] << item[:name]
endruby
This still mutates the hash, but the mutation is local to the accumulator object and the object is introduced exactly where the iteration begins.
Example 3: Summary dashboard ๐ก
Many practical tasks need a summary object with multiple fields:
- total amount;
- count;
- average;
- list of flagged rows.
This is a strong advanced exercise because it shows how accumulator design is a real production concern, not just a coding exercise.
Cheatsheet
Sum with reduce
rows.reduce(0) { |sum, row| sum + row[:amount] }ruby
Build a hash with each_with_object
rows.each_with_object({}) do |row, grouped|
grouped[row[:category]] ||= []
grouped[row[:category]] << row[:name]
endruby
Choose the tool
map: same number of outputs as inputsreduce: collapse many inputs into one resulteach_with_object: build a mutable result object explicitly and locally
Comparison guide โ๏ธ
- external mutable variable: works, but spreads the accumulator outside the loop
reduce: best for totals and single derived valueseach_with_object: best for hashes, arrays, grouped structures
Study Guide
Topic purpose ๐ฏ
Learn to solve "many inputs, one result" problems clearly. This topic should help students see accumulator-based Ruby as a practical everyday technique, not an academic exercise.
Study sequence ๐ช
- Read
overview.md. - Read
shortnote.md. - Study
worked_examples.md. - Keep
cheatsheet.mdopen while readingexample.rb. - Complete the basic exercise.
- Complete the advanced exercise.
Sprint plan ๐
Sprint 1: One total with reduce
- start with a clear initial accumulator;
- make the accumulator type obvious.
Sprint 2: Grouped data with each_with_object
- use a hash accumulator for grouped results;
- keep the accumulator local to the iteration.
Sprint 3: Multi-field summary
- design an accumulator hash that matches the reporting need;
- update multiple fields in one pass over the data.
Optional extension ideas
- nested grouping by two keys;
- converting grouped hashes into sorted arrays;
- combining
filter_mapwith accumulation; - extracting reducer lambdas for reuse.
Reflection questions ๐ค
- Why is
reducea better fit for totals thanmap? - Why is
each_with_objectoften the clearer choice for grouped hashes? - What makes a good accumulator shape for a reporting problem?
Source Files and Tests
Ruby source files are converted directly from the English source tree.
# EXAMPLE CODE
# Topic: topic_15_reduce_accumulators
#
# Purpose:
# - This file demonstrates `reduce` for a simple total.
# - It should pass tests from the beginning.
# - Read it before solving grouped and multi-field accumulator exercises.
class RevenueCalculator
def total_amount(rows)
rows.reduce(0) { |sum, row| sum + row[:amount] }
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_15_reduce_accumulators
#
# What to do:
# - Group item names by category.
# - Use `each_with_object` so the grouped hash is introduced right where it is built.
# - Return the grouped hash instead of printing directly.
#
# Expected outcome:
# - You can solve a grouped-data problem with an explicit accumulator object.
class CatalogGrouper
def names_by_category(items)
items.each_with_object({}) do |item, grouped|
grouped[item[:category]] ||= []
grouped[item[:category]] << item[:name]
end
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_15_reduce_accumulators
#
# Academic purpose:
# - Practice designing an accumulator with more than one field.
# - Show that a single pass can build a useful reporting summary clearly.
#
# Real-world use case:
# - Dashboards and import summaries often need several values at once:
# count, total, average, and a list of flagged rows.
# - This is a realistic place where accumulator design matters.
#
# Why Ruby is beautiful here:
# - A summary hash can be updated in one pass with clear field names.
# - The accumulator shape mirrors the business question.
# - The final result is easy to inspect in tests and logs.
#
# What to do:
# - Build a summary object using `each_with_object`.
# - Track count, total amount, average amount, and expensive item names.
# - Keep the expensive-item rule explicit in the code.
#
# Expected outcome:
# - Advanced tests pass and you can explain why the accumulator has its chosen shape.
class SalesSummary
def summarize(items, expensive_threshold: 100)
summary = items.each_with_object({
count: 0,
total_amount: 0,
average_amount: 0.0,
expensive_names: []
}) do |item, acc|
acc[:count] += 1
acc[:total_amount] += item[:amount]
acc[:expensive_names] << item[:name] if item[:amount] >= expensive_threshold
end
summary[:average_amount] =
summary[:count].zero? ? 0.0 : summary[:total_amount].to_f / summary[:count]
summary
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_15_reduce_accumulators
#
# Solution idea:
# - Start with an empty hash accumulator.
# - Initialize each category bucket lazily.
# - Append names into the right bucket as the iteration proceeds.
class CatalogGrouper
def names_by_category(items)
items.each_with_object({}) do |item, grouped|
grouped[item[:category]] ||= []
grouped[item[:category]] << item[:name]
end
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_15_reduce_accumulators
#
# Solution idea:
# - Use one summary hash as the accumulator.
# - Update all fields in a single pass.
# - Compute the average after the pass so the intermediate logic stays simple.
class SalesSummary
def summarize(items, expensive_threshold: 100)
summary = items.each_with_object({
count: 0,
total_amount: 0,
average_amount: 0.0,
expensive_names: []
}) do |item, acc|
acc[:count] += 1
acc[:total_amount] += item[:amount]
acc[:expensive_names] << item[:name] if item[:amount] >= expensive_threshold
end
summary[:average_amount] =
summary[:count].zero? ? 0.0 : summary[:total_amount].to_f / summary[:count]
summary
end
end
Ruby course source
# This spec is your learning companion for topic_15_reduce_accumulators.
#
# How to use this file:
# 1) Run tests and observe failures or successes.
# 2) Keep the EXAMPLE specs green from the beginning.
# 3) Implement the BASIC exercise as sprint 2: grouped accumulation.
# 4) Implement the ADVANCED exercise as sprint 3: multi-field summary accumulation.
#
# Expected final result:
# - All examples in this file pass.
# - You understand accumulator-driven Ruby as a practical problem-solving style.
require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"
RSpec.describe "topic_15_reduce_accumulators" do
describe "EXAMPLE purpose: understand reduce as a way to collapse many rows into one total" do
it "sums revenue rows" do
calculator = RevenueCalculator.new
expect(calculator.total_amount([{ amount: 40 }, { amount: 60 }, { amount: 20 }])).to eq(120)
end
end
describe "BASIC EXERCISE purpose: group names with each_with_object instead of manual setup" do
it "returns names grouped by category" do
grouper = CatalogGrouper.new
items = [
{ category: "books", name: "Ruby Book" },
{ category: "books", name: "Refactoring" },
{ category: "home", name: "Desk Lamp" }
]
expect(grouper.names_by_category(items)).to eq(
{
"books" => ["Ruby Book", "Refactoring"],
"home" => ["Desk Lamp"]
}
)
end
end
describe "ADVANCED EXERCISE purpose: build a dashboard-style summary in one pass" do
it "tracks count, total, average, and expensive item names" do
summary = SalesSummary.new
items = [
{ name: "Ruby Book", amount: 80 },
{ name: "Refactoring", amount: 120 },
{ name: "Desk Lamp", amount: 150 }
]
expect(summary.summarize(items, expensive_threshold: 100)).to eq(
{
count: 3,
total_amount: 350,
average_amount: 116.66666666666667,
expensive_names: ["Refactoring", "Desk Lamp"]
}
)
end
end
end
Ruby course source
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
Ruby course source
Study Prompts
Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.
Attempt the exercises before opening the answer files, then compare the design choices.