15

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 reduce fits better than map;
  • use each_with_object to 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]
end
shortnote.md
ruby

Ruby accumulator style can look like this:

rows.reduce(0) { |sum, row| sum + row[:amount] }
shortnote.md
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]
end
shortnote.md
ruby

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:

  • reduce can 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_object often easier to read than reduce for hash-building tasks?

Worked Examples

Example 1: Invoice total ๐Ÿ’ก

Old style:

total = 0
invoices.each do |invoice|
  total += invoice[:amount]
end
worked_examples.md
ruby

Accumulator style:

invoices.reduce(0) { |sum, invoice| sum + invoice[:amount] }
worked_examples.md
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]
end
worked_examples.md
ruby

Ruby style:

items.each_with_object({}) do |item, grouped|
  grouped[item[:category]] ||= []
  grouped[item[:category]] << item[:name]
end
worked_examples.md
ruby

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] }
cheatsheet.md
ruby

Build a hash with each_with_object

rows.each_with_object({}) do |row, grouped|
  grouped[row[:category]] ||= []
  grouped[row[:category]] << row[:name]
end
cheatsheet.md
ruby

Choose the tool

  • map: same number of outputs as inputs
  • reduce: collapse many inputs into one result
  • each_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 values
  • each_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 ๐Ÿชœ

  1. Read overview.md.
  2. Read shortnote.md.
  3. Study worked_examples.md.
  4. Keep cheatsheet.md open while reading example.rb.
  5. Complete the basic exercise.
  6. 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_map with accumulation;
  • extracting reducer lambdas for reuse.

Reflection questions ๐Ÿค”

  • Why is reduce a better fit for totals than map?
  • Why is each_with_object often 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
en/topic_15_reduce_accumulators/example.rb
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
en/topic_15_reduce_accumulators/basic_exercise.rb
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
en/topic_15_reduce_accumulators/adv_exercise.rb
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
en/topic_15_reduce_accumulators/answer_basic_exercise.rb
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
en/topic_15_reduce_accumulators/answer_adv_exercise.rb
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
en/topic_15_reduce_accumulators/tests/topic_15_reduce_accumulators_spec.rb
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}")"
en/topic_15_reduce_accumulators/run_topic_tests.sh
Ruby course source

Study Prompts

  1. Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.

  2. Attempt the exercises before opening the answer files, then compare the design choices.