Mastering the reduce function

When you first get started learning about functional programming in JavaScript, there are a few functions you grasp pretty easily. Array.prototype.filter is very intuitive, especially if you have worked with a language like SQL which is all about filtering out data. Array.prototype.map is a bit more complex, but you can figure it out pretty quickly if you're given a few examples.

Then there is Array.prototype.reduce.

Also known as fold, inject, aggregate, accumulate, or compress in languages like Haskell, Clojure, and Lisp, reduce is a weird function. For the longest time, I struggled to really grasp how the reduce function works. I think the reason why is because reduce tries to do a few things at once. Whereas filter removes items that fail your test, and map modifies your data into a new dataset, reduce kind of does a mishmash of a few things.

The reduce function can be very confusing

The examples in places like MDN are overly simplistic - they basically show reduce as a way of doing addition by folding one result onto the next. Which is fine, but stumps people the moment you aren't working with numbers. Here's how it usually looks:

// Summation of numbers in an array
[1, 2, 3].reduce((sum, n) => sum + n);

This is a perfectly valid example, but you might be wondering: How do you reduce an array of strings? Why would you reduce an array of strings? What are some real-world examples of using reduce to help you in your day-to-day work? We're going to attempt to answer those in this article so you can master the reduce function and finally feel comfortable using it.

Definition and breakdown

Reduce's job is to take a collection of items and reduce them down to one singular value. The type signature of this function looks like this:

reduce: (callback: function, initialValue?: any) => any
callback: (accumulator: any, currentValue: any, currentIndex?: number, array?: array) => any

We are using TypeScript's function type annotation syntax for this example. In short, it just helps decode the inputs and the outputs. So our function takes in two arguments we've called callback and initialValue. What are those arguments?

callback argument and its type signature

callback is a function that takes four arguments itself: accumulator, currentValue, currentIndex, and array.

accumulator is of the type of thing you're trying to make your outputValue. So if you're reducing an array of numbers through addition, the accumulator is a number. It is accumulating, or growing, in size not by the length of the array but by the "growth" of the eventual output value. In fact, there is an inverted relationship by how the accumulator grows against how the array is reduced. This object is definitely the most confusing, so if this still doesn't make sense, we'll break this down in the later examples.

currentValue is an item in the array you're trying to reduce. As you iterate over the array, your currentValue changes as you move from the first to the last element in that array.

currentIndex is an optional number argument that coincides with the currentValue by expressing that value's index. So as you are iterating over your initial array, the currentIndex will grow from 0 up to the n-1 number of elements in your array.

array is an optional Array argument that is the entire input array you provided to the reduce function. This might seem like an odd thing to pass in but the key here is that the input array is not modified. Functional methods obey immutability, meaning they do not modify the array object they are provided. Therefore, regardless of what you do in your reduce method, you always have access to your input array because it is never changed throughout the execution of reduce.

initialValue argument

initialValue is an optional argument that provides the initial value for accumulator to begin reducing. For example, if you wanted to reduce [1,2,3] into the summation of those values, you'd want the initialValue to be set to 0 to ensure you are adding against the same type (a number) and that this value does not pollute the values that are being manipulated by the array. If you do not set this argument, the accumulator will be set to the first value of the array. In the above example, that would be 1.

outputValue return object

outputValue is the final form of the accumulator object once the entire array has been iterated. Unlike most array functional methods, which return arrays, this will return the type that you have built throughout the iteration. This is not necessarily the same type as the values within your input array, as we will see below.

Examples

Now that we know what goes in and out of the reduce function, let's dive into a few examples, step-by-step, in increasing difficulty.

Easy - sum elements of an array

As we mentioned earlier, the ever-present example of reducing arrays is to sum a list of numbers through addition. It can be succinctly expressed like this:

[1, 2, 3].reduce((sum, n) => sum + n);

The easiest way to see how the iteration is working is to throw log statements at each step for our two input arguments:

[1,2,3].reduce((a, c) => {
  console.log(`accumulator: ${a}`);
  console.log(`currentValue: ${c}`);
  return a + c;
});

// FIRST ITERATION
// accumulator:  1
// currentValue: 2
// SECOND ITERATION
// accumulator:  3
// currentValue: 3
// FINAL ITERATION (AS RETURN VALUE)
// 6

On the first iteration, we grab the first element in our array, 1, and set that as our accumulator value. We then add our currentValue, which is the second element in the array, 2, onto our accumulator, because that is the logic we have decided on for our callback function. 1+2 equals 3, so 3 therefore becomes our new accumulator value in our second iteration. Since no initialValue is set, our second iteration is actually operating on the third and final element, 3, which is added on to our new accumulator value of 3. 3+3 equals 6, and with no elements left to iterate on in our array, we return the final accumulator value of 6.

Easy! Now, what happens if we operate on another kind of type, like a string?

Medium - simple string builder

One of the most common things I see in JavaScript code involves building strings like this:

const myBuddies = ['James', 'Darien', 'Eric'];
let greeting = 'Greetings friends! ';

for (let i = 0, l = myBuddies.length; i < l; i++) {
  greeting += myBuddies[i] + ' is my buddy. ';
}

greeting += 'These are all of my buddies!';

return greeting;

You start with an array and an empty string. You build the string up from the array elements, maybe adding in some additional information, and then you return the new string, fully formed and populated. Now look what happens when I rearrange and rename some variables, do you see the connection?

const array = [some, array, values, ...rest];
const initialValue = 'Greetings friends! ';
let accumulator = typeof initialValue === "undefined" ?
  initialValue :
  array[0];
const manipulation = (a, b) => a.concat(b);

let i = (typeof initialValue === "undefined" ? 0 : 1);
let l = arr.length;
for (i, l; i < l: i++) {
  accumulator = manipulation(accumulator, array[i]);
}

return accumulator;

This iterative pattern is the reduce function. The callback within reduce is simply some function of manipulation of data, iterated across an array and saved into some built value called an accumulator! The difference is, our last example is 8 lines of code, and to make this in reduce, we can express it in a very clean and terse way, with zero local variables needed:

['James', 'Darien', 'Eric']
  .reduce((paragraph, name) => {
    return `${paragraph}${name} is my buddy. `;
  }, 'Greetings friends! ')
  .concat('These are all of my buddies!');

This is pretty neat. We were able to remove all local variables and reduce the line count by more than half. We are also able to chain functions together to continue to tack on unique statements at the end of our string builder.

While this instance is a bit more involved and doesn't simply manipulate numbers, at the end of the day, this is still a form of summation, which may seem a bit too simplistic and similar to the first example. Lucky for you, we've got one last example that should be a bit more advanced and is not simply a form of addition.

Hard - flatten array to dictionary

Our last example is not going to simply be some form of summation. Instead, we're going to build a dictionary (also known as a hash table, which is simply an Object in JavaScript) from an array of arrays.

We're going to make the first value in our sub-array be the keys, and the rest of our values in our sub-array be the values. If there are hash collisions (meaning multiple sub-arrays with the same value in the first element), we simply add the remaining sub-array items onto the existing key/value pairing.

How are we possibly going to do this with reduce?

We know our initial value is {}. We aren't doing a summation (or concatenation if we're dealing with strings), but we are still trying to manipulate our output value, which we know to be an Object as a hash table. How can we add things onto a hash table in JavaScript? Square bracket notation! That should be enough to get us what we want:

const arrayOfArrays = [
  ['a', 'ace', 'apple', 'axe'],
  ['b', 'baseball', 'boy', 'bye'],
  ['c', 'cat', 'cold'],
  ['a', 'azure'],
  ['a', 'azure']
];

const dictionary = arrayOfArrays.reduce((dict, page) => {
  const [letter, ...words] = page;

  dict[letter] = dict.hasOwnProperty(letter) ?
    dict[letter].concat(
      words.filter(word => !dict[letter].includes(word))
    ) :
    words;

  return dict;
}, {});

dictionary['a'][0];
// 'ace'
dictionary['a'][dictionary['a'].length - 1];
// 'azure'
dictionary['a'].filter(word => word === 'azure').length
// 1

Lots to unpack here. To start, we created our multi-dimensional array called arrayOfArrays. Each row in the array starts with the letter of our dictionary. That letter will represent the various pages in our dictionary. You could do this without declaring the local variable, but for the sake of readability within this blog, I chose to separate them.

Next, we construct our actual dictionary. We start with our initial Object, a simple empty {} hash. Our callback represents two things, the dict dictionary object that we are constructing, and the page, which represents a row in our arrayOfArrays. That row has two kinds of strings: the letter representing what class of words we're on, and those words listed on that page.

In languages like Haskell, lists come with functions called head and tail (also known as car and cdr in older languages like Lisp). In JavaScript, these don't come out of the box, but thankfully in ES6 we can quickly grab these through the destructuring pattern [first, ...rest] = array. We do exactly this to grab our letter and corresponding words.

Next, we have to build our dictionary. There are two major considerations here: new pages and existing pages.

If we have a new page, we are dealing with the simpler case. All we have to do is populate the page (whose key is letter) with our words.

If we are dealing with an existing page, we want to add onto that existing page using concat. But we can't simply concatenate our list of words. What if we've already added a word to our dictionary? That's where our functional cousin filter comes in to filter out words that are already included on this page. The filter will return only novel words, removing duplicates along the way.

Finally, we test out our new dictionary variable to prove our dictionary is complete, can handle adding words late to our prepopulated keys, and will gracefully handle duplicates.

Final thoughts

Congratulations! You now know how to reduce an array down to a composite value. You've not only mastered the reduce function in JavaScript but in every other language.

As we mentioned in the beginning, the concepts behind reduce are the same across languages like Clojure and Haskell, but with different names like fold or inject. You can also now reason about variants on reduce, such as foldr (i.e. fold right, which is the same as reduce), or foldl (i.e. fold left, similar to reduce except the iteration happens from the back to the front of the array).

If you're still confused, I've failed you. But fear not; find me on Twitter and I'd be happy to provide even more examples. I will not rest until everyone reading this knows the power of reduce!


Get the FREE UI crash course

Sign up for our newsletter and receive a free UI crash course to help you build beautiful applications without needing a design background. Just enter your email below and you'll get a download link instantly.

A new version of this app is available. Click here to update.