Hung-Yi’s Journal

Front-end Developer, Emacs Adventurer, Home Cook

28 Sep 2020

JavaScript's Reduce: A Swiss Army Knife for Arrays

There's a little Array method in JavaScript that I've seen a lot of developers overlook. I suspect it's because of its abstract nature that doesn't exactly lead developers to any obvious use cases. But that's why I think it's a great programming Swiss Army knife: you can do a lot more with it than you might realise1. Let me introduce you to my friend, Array.prototype.reduce(). Today, I'll show you some creative ways to use it that may just inspire you.

The Basics

There are 2 key ideas to understanding reduce():

  1. It loops through every item in an array, one by one; and

  2. It accumulates results (by calling a function that you define) and spits it out at the end as the return value.

To remind myself of these 2 core concepts, I usually use a lambda with the input parameters as acc (a.k.a. accumulated results) and curr (a.k.a. current item of the array that we're looping through). The names of the local variables help set the context in my head that I'm writing a reducer function. That said, feel free to name them however you please.

The Classic Example

Summing a list of numbers is an example that's been repeated ad nauseam2 on every page of documentation ever written on fold/reduce/accumulate/aggregate/compress/inject:

// Calculate the sum of a list of numbers
[1,2,3,4].reduce((acc, curr) => acc + curr, 0); // => 10

Admittedly, it's a great use case for reduce. But because this example is so common, I'm sure many people think numerical summation or aggregation is the only strong suit of reduce; I assure you it's not!

First let's take a detour to give you a taste of how fundamental the concept of reduce actually is.

More Primitive Than Map, Filter, Includes & Find

You may be familiar with array functions such as map, filter, includes and find already. These are the bread and butter for list processing in JavaScript. I use them all the time, of course.

But did you know that you can implement them using reduce? That's right. Because reduce is a more primitive and flexible building block for list processing, it can do everything that the friendlier functions can.

With the parts for an IKEA shelf, you can build that IKEA shelf. But with raw lumber, the right tools and some considerable effort, you can build almost anything.

Let me show you…

Implementing map using reduce:

const input = [1.618, 2.718, 3.141592653, 2.5029];

return {
  // Use map() to round a list of real numbers to the nearest integer
  roundedMap:
    input.map(x => Math.round(x)),

  // Do the same using reduce()
  roundedReduce:
    input.reduce((acc, curr) => [...acc, Math.round(curr)], [])
};
{ roundedMap: [ 2, 3, 3, 3 ], roundedReduce: [ 2, 3, 3, 3 ] }

Implementing filter using reduce:

const input = [1.618, 2.718, 3.141592653, 2.5029];

return {
  // Use filter() to find all numbers above 2
  above2Filtered:
    input.filter(x => x > 2),

  // Do the same using reduce()
  above2Reduced:
    input.reduce((acc, curr) => curr > 2 ? [...acc, curr] : acc, [])
};
{
  above2Filtered: [ 2.718, 3.141592653, 2.5029 ],
  above2Reduced: [ 2.718, 3.141592653, 2.5029 ]
}

Implementing includes using reduce:

const input = [1.618, 2.718, 3.141592653, 2.5029];
const pi = 3.141592653;

return {
  // Use includes() to see if the collection contains pi
  containsPiIncludes:
    input.includes(pi),

  // Do the same using reduce()
  containsPiReduced:
    input.reduce((acc, curr) => acc || curr === pi, false)
};
{ containsPiIncludes: true, containsPiReduced: true }

Implementing find using reduce:

const input = [1.618, 2.718, 3.141592653, 2.5029];

return {
  // Use find() to get the first item that is larger than 2
  firstAbove2Find:
    input.find(x => x > 2),

  // Do the same using reduce()
  firstAbove2Reduced:
    input.reduce((acc, curr) => acc || (curr > 2 ? curr : undefined), undefined)
};
{ firstAbove2Find: 2.718, firstAbove2Reduced: 2.718 }

Note that the above implementations are for demonstration purposes only. They're not as optimized and definitely less readable than their simpler counterparts. Do not blindly replace everything with reduce.

However, if customizing the use of reduce above allows you to achieve something that you couldn't before, then by all means reduce away! Which brings us to…

Things That Are Definitely Easier With Reduce

I'll show you a few creative uses for reduce that you might not have thought of, but this is by no means an exhaustive list. reduce is too flexible of a tool to have a finite list of use cases.

As long as you have to solve a problem with a list or array, you should consider reduce if there's no other readily-available way to do it.

Example 1: Key-value Pair Aggregation

Sometimes you'll find yourself with an array of key-value pairs that you would rather have as one single JavaScript object instead. There are several ways to do this3, but reduce can be a really good, idiomatic choice.

const pairs = [
  { 'key': 'apple',  'value': 5  },
  { 'key': 'orange', 'value': 3  },
  { 'key': 'banana', 'value': 10 }
];

return pairs.reduce((acc, curr) => ({...acc, [curr.key]: curr.value}), {});
{ apple: 5, orange: 3, banana: 10 }

Example 2: Cumulative Arithmetic

The accumulator pattern that reduce uses makes it intuitive to do cumulative sums, since you have immediate access to the accumulated data at each step. You also have easy access to the loop index (declared as i below).

const input = [1,2,3,4,5,6,7,8,9,10];
return input.reduce((acc, curr, i) => [
  ...acc,
  i === 0 ? curr : acc[i-1] + curr
], []);
[
   1,  3,  6, 10, 15,
  21, 28, 36, 45, 55
]

Example 3: Group By

In plain JavaScript there's no obvious way to do group-by on a key. Your options are:

  1. Use an external library like Lodash

  2. Loop through manually and build your own map or JavaScript object

  3. Use reduce

I think the most idiomatic way is to use reduce since it avoids side effects and doesn't leave behind any garbage assignments to clean up. With reduce you also don't have to rely on third party libraries that might add bloat to your code too.

This is how:

const ingredients = [
  { name: 'celery',     category: 'fiber'   },
  { name: 'potato',     category: 'carb'    },
  { name: 'egg',        category: 'protein' },
  { name: 'flour',      category: 'carb'    },
  { name: 'butter',     category: 'fat'     },
  { name: 'spinach',    category: 'fiber'   },
  { name: 'bread',      category: 'carb'    },
  { name: 'mayonnaise', category: 'fat'     },
  { name: 'chicken',    category: 'protein' },
];
// Let's group by the category into a JavaScript Map using reduce()
return ingredients.reduce((acc, curr) => {
  const existingGroup = acc.get(curr.category);
  return acc.set(
    curr.category,
    existingGroup ? [...existingGroup, curr] : [curr]
  ); // Map.set() returns the Map itself for convenience
}, new Map());
Map(4) {
  'fiber' => [
    { name: 'celery', category: 'fiber' },
    { name: 'spinach', category: 'fiber' }
  ],
  'carb' => [
    { name: 'potato', category: 'carb' },
    { name: 'flour', category: 'carb' },
    { name: 'bread', category: 'carb' }
  ],
  'protein' => [
    { name: 'egg', category: 'protein' },
    { name: 'chicken', category: 'protein' }
  ],
  'fat' => [
    { name: 'butter', category: 'fat' },
    { name: 'mayonnaise', category: 'fat' }
  ]
}

Example 4: Structural Transformation

Actually, reduce is not not limited to mapping one-to-one or reducing to a single return value. Just like how filter can return fewer items than the original array, we can extend this concept to do all sorts of structural transformations and collapsing.

For example, let's pair up this single list of people into subgroups of 2:

const people = [
  'Alice', 'Bob',    'Charlie', 'Daisy', 'Edna',
  'Fara',  'Gordon', 'Hubert',  'Iris',  'Julian'
];
const groupsOf = 2;
return people.reduce((acc, curr, i) => {
  if (i % groupsOf === 0) {
    acc.push([curr]);
  } else {
    acc[acc.length - 1].push(curr);
  }
  return acc;
}, []);
[
  [ 'Alice', 'Bob' ],
  [ 'Charlie', 'Daisy' ],
  [ 'Edna', 'Fara' ],
  [ 'Gordon', 'Hubert' ],
  [ 'Iris', 'Julian' ]
]

Bonus Example: Redux-style Actions and Reducers

I'm pretty sure the idea of redux reducers came from the general concept of functional reduction. In fact, it would be correct to call the lambda function inside the reduce examples above as "reducers", since that function's job is to do the reducing.

When you reduce over some actions, you're essentially looping over them and accumulating their effects on some application state. Here's a toy calculator that follows this pattern.

const actionTypes = {
  ADD:      0,
  SUBTRACT: 1,
  DIVIDE:   2,
  MULTIPLY: 3
};
const actions =  [
  { type: actionTypes.ADD,      payload: 5  },
  { type: actionTypes.SUBTRACT, payload: 1  },
  { type: actionTypes.MULTIPLY, payload: 8  },
  { type: actionTypes.MULTIPLY, payload: 32 },
  { type: actionTypes.SUBTRACT, payload: 24 },
  { type: actionTypes.DIVIDE,   payload: 5  },
  { type: actionTypes.ADD,      payload: 2  },
  { type: actionTypes.MULTIPLY, payload: 10 },
];
const initialState = [0];
return actions.reduce(
  (state, action, i) => {
    switch (action.type) {
      case actionTypes.ADD:
        return [...state, state[i] + action.payload];
      case actionTypes.SUBTRACT:
        return [...state, state[i] - action.payload];
      case actionTypes.DIVIDE:
        return [...state, state[i] / action.payload];
      case actionTypes.MULTIPLY:
        return [...state, state[i] * action.payload];
    }
  },
  initialState
);
[
     0,    5,   4,  32,
  1024, 1000, 200, 202,
  2020
]

A New Tool for Your Tool-belt

I hope I've shown that reduce is worth the effort to consider when dealing with lists and arrays, especially when none of the existing array functions do exactly what you want.

It's a primitive tool that can be a little mind-bending at first—but if you give it a little time, it can pay you back with its flexibility and elegance, allowing you to use one generic software pattern to solve many different types of problems.


1

And just like a Swiss Army knife, just because you can do almost everything with it doesn't mean you should. Don't try to fell a tree with a Swiss Army knife; and don't try to use reduce to sort an array.

2

I'm pretty sure a poor transistor dies a painful death each time a summation example is used to explain reduce().

3

If your data is in the right format, you can use Object.fromEntries(). If it isn't in the right format, you can map it into the right format beforehand, but by then you might as well use reduce and get it done in one step. You might also consider new Map(pairs) if you're using the object as a dictionary-like lookup.