Functional Basics #3: Reduce

kvsm

Kevin Smith 🏴󠁧󠁢󠁳󠁣󠁴󠁿

Posted on October 30, 2019

Functional Basics #3: Reduce

Welcome back! In the third part of this mini-series on functional programming basics, I'll be taking a more in-depth look at a slightly more complex array function -reduce.

If you haven't already read it, I recommend starting with the first article in this series, Functional Basics #1: Map. I'll be building on some ideas as the series progresses.

What is it?

Let's recap on the array functions we've looked at so far:

  • map runs a function on each element of an array, and returns an array of the results.
  • filter checks each element of an array by running a predicate function on it, and returns an array with only the elements for which the predicate returns true.

So what's reduce? Well, like the name suggests:

  • reduce runs a function on each element of an array, and reduces the array to a single value.

Cue a simple example - we have an array of numbers, and we want to add the square of each number to get a total. Before we look at how reduce could do this, let's just implement it ourselves, in imperative style, with a for loop:

const myArray = [2, 9, 34, 22]
let total = 0

for (let i = 0; i < myArray.length; i++) {
  const num = myArray[i]
  const square = num * num
  total += square
}

// total: 1725

If you've been following this series, you know the drill here - let's refactor the calculation out of the for loop:

const myArray = [2, 9, 34, 22]
let total = 0

const addSquare = (total, num) => total + num * num

for (let i = 0; i < myArray.length; i++) {
  const num = myArray[i]
  total = addSquare(total, num)
}

// total: 1725

The addSquare function which we've extracted here is known as a reducer function. It takes the current total and the next number in the array, and returns the new total. This means that if we apply this function to every number in the array, passing in the running total each time, we'll eventually reduce the array down to a single value - the final total.

Here's how that looks using reduce:

const myArray = [2, 9, 34, 22]

const addSquare = (total, num) => total + num * num

const total = myArray.reduce(addSquare, 0)

// total: 1725

So much cleaner! There's a couple of things to note here:

  • The first argument to reduce is our reducer function, addSquare.
  • The second argument (0 in this case) is the initial value of total. This is equivalent to where we had previously used let total = 0.

Getting the right initial value is important! If we miss it out, Javascript will take the first element in the array as the initial value instead of running our reducer on it, and we'll get the wrong result:

const myArray = [2, 9, 34, 22]

const addSquare = (total, num) => total + num * num

const total = myArray.reduce(addSquare)

// total: 1723 - wrong!

A brief interlude for some terminology:

  • The total value, the first argument to a reducer function, is more generally known as the accumulator - it 'accumulates' the result of running the reducer on each array element. Commonly shortened to acc.
  • num here is referred to as the current element, shortened to cur.

Important to note that a reducer should always return the value which will be the acc for the next iteration. Therefore, the definition for ALL reducer functions should look like:

(acc, cur) => {
  // do stuff here
  return nextAccValue
}

What else can reduce do?

It can build anything! You can use it to generate any value - a number, a string, an object, even another array (because an array itself is a value). Here's an example to categorise words into an object by their length:

const words = ['the', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog']

const result = words.reduce((acc, cur) => {
  // If the key for this word length doesn't exist yet, make it an empty array
  if (!acc[cur.length]) {
    acc[cur.length] = []
  }
  // Add the current word to the right key for its length
  acc[cur.length].push(cur)
  return acc
}, {})

/* result:
{
  '3': [ 'the', 'fox', 'the', 'dog' ],
  '4': [ 'over', 'lazy' ],
  '5': [ 'quick', 'brown' ],
  '6': [ 'jumped' ]
}
*/

Note how this time the reducer function is passed directly to reduce as an anonymous function - this is common. Also note that the initial value is an empty object, {}. The reducer takes this empty object and builds the result by adding keys and values as it goes.

Another handy use of reduce is to avoid having to iterate over an array more times than necessary. Assume we have an array of numbers which we want to:

  • multiply by 4
  • then filter to only include numbers greater than 20
  • then sum the total

We could do it like this:

const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]

const times4 = num => num * 4
const greaterThan20 = num => num > 20
const sum = (acc, cur) => acc + cur

const result = nums
  .map(times4)
  .filter(greaterThan20)
  .reduce(sum, 0)

// result: 120

This looks nice and clean gives the right result, but has a problem - it loops over the array three times, once each for map, filter and reduce. This could have a noticeable impact on the performance of our code if we're processing very large arrays. Instead, we can refactor the map, filter and reduce into a single reducer which does everything required in one loop of the array:

const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]

const result = nums.reduce((acc, cur) => {
  // Multiply by 4
  const product = cur * 4
  // If not greater than 20, filter this number out by returning the acc unchanged
  if (!(product > 20)) {
    return acc
  }
  // Otherwise return the sum of the acc and the product
  return acc + product
}, 0)

// result: 120

From this we can begin to see how flexible and powerful reduce can be. Since we have replaced a map and a filter operation with a single reducer, it's apparent that we could actually write our own version of the map and filter functions using only reduce (if you're up for the challenge, give it a go and post your solution in the comments!)

Hope this has been helpful, stay tuned for more articles on functional programming soon!

If you found this article helpful, follow me! I'll be adding more articles soon. Liked it? Like it ❤️! Suggestions/improvements? Comment ⬇️! :)

💖 💪 🙅 🚩
kvsm
Kevin Smith 🏴󠁧󠁢󠁳󠁣󠁴󠁿

Posted on October 30, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related