Why a Single‑Pass Accumulation Matters

When I work with collections of objects—think invoices, log entries, or order lines—I often need to produce a summary that groups related data together. The naive approach is to loop over the data, update a hash, and then loop again to compute totals. That results in multiple passes over the same dataset, which can be wasteful with large files or real‑time streams.

In my experience, the Ruby community already gives us a clean solution: each_with_object. It lets us accumulate a result in a single traversal, keeping the code readable and the performance respectable. Below I’ll walk you through the technique, a realistic scenario, and a production‑ready snippet you can drop into your Gemfile right away.

The `each_with_object` Technique

At its core, `each_with_object` is a convenience wrapper around inject (also known as reduce) that supplies an initial empty object—usually a hash. Instead of writing:

result = {}
items.each do |item|
  result[item[:category]] = (result[item[:category]] || 0) + item[:amount]
end

you can write:

result = items.each_with_object({}) do |item, memo|
  memo[item[:category]] = (memo[item[:category]] || 0) + item[:amount]
  memo
end

The block receives the accumulator (memo) as the second argument, and we can mutate it in place. The method returns the accumulator after the iteration, so you can chain further operations if needed.

Real‑World Scenario: Summarizing Order Data

Imagine I’m building an e‑commerce dashboard that needs to show revenue broken down by product category for the last week. I receive an array of order line items from an external API:

order_lines = [
  { id: 1, category: "electronics", amount: 250.00 },
  { id: 2, category: "books", amount: 35.00 },
  { id: 3, category: "electronics", amount: 150.00 },
  { id: 4, category: "clothing", amount: 80.00 },
  { id: 5, category: "books", amount: 22.00 },
  { id: 6, category: "electronics", amount: 99.99 }
]

The goal is to produce a hash like:

{
  "electronics" => 499.99,
  "books" => 57.00,
  "clothing" => 80.00
}

Using `each_with_object` we can achieve this in one pass, which is both intuitive and efficient for larger datasets.

Pro tip: If you ever need to nest the summary (for example, by month and category), you can start with a hash of hashes and still keep a single traversal.

Production‑Ready Example

Below is a complete, commented snippet that you could paste into a service object or a model method. It handles missing categories, normalizes amounts to cents to avoid floating‑point quirks, and returns a clean hash with stringified keys.

# frozen_string_literal: true

# Build a revenue summary from an array of order lines.
# @param order_lines [Array] each hash must contain :category and :amount
# @return [Hash] keys are category names, values are total revenue in cents
# @example
#   summarize_revenue(order_lines)
#   # => {"electronics"=>49999, "books"=>5700, "clothing"=>8000}

def summarize_revenue(order_lines)
  # Start with an empty hash as the accumulator
  order_lines.each_with_object({}) do |line, memo|
    # Ensure the category is a string for consistent keys
    key = line[:category].to_s
    # Convert amount to integer cents (multiply by 100 and round)
    cents = (line[:amount] * 100).round
    # Use `fetch` with a default of 0 to add up existing totals
    memo[key] = memo.fetch(key, 0) + cents
    # Return memo (implicitly) – required for `each_with_object`
    memo
  end
end

# Example usage
order_lines = [
  { category: "electronics", amount: 250.00 },
  { category: "books", amount: 35.00 },
  { category: "electronics", amount: 150.00 },
  { category: "clothing", amount: 80.00 },
  { category: "books", amount: 22.00 },
  { category: "electronics", amount: 99.99 }
]

summary = summarize_revenue(order_lines)
puts summary.inspect
# => {"electronics"=>49999, "books"=>5700, "clothing"=>8000}

The function is pure Ruby, side‑effect free, and works with any collection that responds to `each`. Adding a second level of grouping (e.g., by month) is as simple as initializing a nested hash and updating the inner memo.

When to Prefer `each_with_object` Over `inject` or `reduce`

At first glance `each_with_object` looks like a synonym for `inject`. The distinction is subtle but useful:

  • Clarity: When the accumulator is an object you want to keep (like a hash), naming it memo makes the intent obvious.
  • Performance: Both methods are implemented in C, so there’s no measurable difference. However, `each_with_object` guarantees the accumulator is not nil, which can prevent accidental `NoMethodError` bugs.
  • Readability: If you have multiple loops over the same collection and need separate summaries, you can chain calls to `each_with_object`, keeping each transformation isolated.

Use `inject` when you need a single primitive value (like a sum) and want to avoid creating an intermediate hash. In all other cases, `each_with_object` is a clean, expressive choice.

Key Takeaways

  • `each_with_object` provides a readable way to accumulate a hash (or any object) in a single pass over a collection.
  • It is ideal for grouping, counting, or summarizing operations where you need a mutable accumulator.
  • Start with an empty hash `{}` and let the block update it; the method returns the accumulator automatically.
  • Combine it with other Ruby idioms—like fetch for safe defaults—to write production‑ready code quickly.
  • Remember that `each_with_object` is not a performance monster; its biggest win is code clarity and the guarantee of a non‑nil accumulator.

Integrating this pattern into daily work has saved me from writing verbose loops and has made my intent crystal clear to teammates. Give it a try on your next grouping task—you’ll likely find it just as powerful as I do.