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 returnstrue
.
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 oftotal
. This is equivalent to where we had previously usedlet 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 toacc
.num
here is referred to as the current element, shortened tocur
.
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 ⬇️! :)
Top comments (1)
Great article, really helpful. Thanks 🙏