---
title: How to approach a reduce problem
teaser: Code written with `reduce` can be intimidating. This 2-step approach can help.
tags: web,ruby,functional programming
author: Joël Quenneville
published_on: 2024-03-13
---

Ruby's [`reduce` (aka `inject`)] can be intimidating. It can be hard to both read and to write.
This handy two-step approach has helped me write `reduce` code without tying my
brain in knots.

## Two-step process

Here are the two steps:

1. Figure out how to combine 2 items
2. Use `reduce` to scale up to _n_ items

They derive from a helpful mental model I have:

> `reduce` is a tool for scaling a method that combines 2 items into a method
> that combines _n_ items.

## Problem: Aggregating T-shirt inventory

Consider some code that models multiple warehouses that hold inventory of
various sized t-shirts. We might want to find the total inventory across all
warehouses. Aggregation problems commonly call for `#reduce` but this one could get rather gnarly.

```ruby
Warehouse = Struct.new(:name, :stock)

warehouses = [
  Warehouse.new("East", {small: 1}),
  Warehouse.new("West", {small: 1, medium: 2, large: 1})
]
```

## Step 1 - figure out how to combine two values

The first step is to make the problem smaller. Don't try to figure out how to
aggregate a whole list of inventories. Instead let's start by writing a method
to sum two inventories.

Because inventories are hashes, we can use [`Hash#merge`][`Hash#merge`] to combine them. We
use the block form to decide what to do when both hashes contain the same key
(e.g. both have an entry for `:small`). Here we want to add them.

```ruby
def add_inventories(inventory1, inventory2)
  inventory1.merge(inventory2) do |key, val1, val2|
    val1 + val2
  end
end
```

## Step 2 - use `reduce` to scale operation to _n_ items

Now that we have a way to add two inventories, we can use `#reduce` to apply
our `add_inventories` method to a whole array. Job done!

```ruby
warehouses.reduce({}) do |running_total, warehouse|
  add_inventories(running_total, warehouse.stock)
end
```

## Bonus points: extract value object

For extra niceness we could replace our hash of t-shirt sizes with a proper
`Inventory` object. Blocks on enumerable collections often [want to be instance
methods](https://thoughtbot.com/blog/avoid-putting-logic-in-map-blocks) on the
items in the collection, especially when those items are primitives like the
hashes we've been using here. Now we have a place for that `add_inventories`
method to live ([that suffix was probably a big
hint!](https://thoughtbot.com/ruby-science/feature-envy.html)) and we can even
name it `+`.

```ruby
Inventory = Data.define(:small, :medium, :large) do
  def self.empty
    new(small: 0, medium: 0, large: 0)
  end

  def +(other)
    Inventory.new(
      small: self.small + other.small,
      medium: self.medium + other.medium,
      large: self.large + other.large
    )
  end
end
```

And now the scary block entirely disappears (see [this article for how the non-block form works])!

```ruby
warehouses.map(&:stock).reduce(:+)
```

## Similar patterns: Monoids and Semigroups

This way of thinking is similar to two concepts from abstract algebra:
[semigroups] and [monoids]. The core idea (slightly oversimplified) is that you
have an object that:

Defines a **combining method** (e.g. `Inventory#+`) that combines two instances.
This method must be order-independent (e.g. `a + (b + c) == (a + b) + c`).
Notice that this is basically my step 1 above!

Defines an **"empty" or "neutral" value** (e.g. `Inventory.empty`)
that does nothing when combined with another instance using the combining method
defined above. For example adding an empty inventory to an existing one is the
same as doing nothing `some_inventory + Inventory.empty == some_inventory`.

It turns out that arrays of these kinds of objects are inherently reduceable.
You plug in the empty value as the base case and pass a symbol for the combining
method and it all just works!

```ruby
array_of_monoids.reduce(YourMonoidalObject.empty, :combine)
```

[`reduce` (aka `inject`)]: https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-reduce

[`Hash#merge`]: https://ruby-doc.org/3.2.2/Hash.html#method-i-merge

[this article for how the non-block form works]: https://thoughtbot.com/blog/blocks-procs-and-enumerable
[semigroups]: https://typeclasses.com/semigroup
[monoids]: https://blog.ploeh.dk/2017/10/06/monoids
