Today we will master map
, filter
, and reduce
by deconstructing and rebuilding them from scratch.
When I was little, I received a watch as a gift. Much to my mother's horror, the first thing I did was grab the smallest screwdriver I could find and take it apart piece by piece. I wanted to see the internals and inspect every part.
To my mother's relief, I was able to put the watch back into it's original working state. Having thoroughly inspected the internals, I walked away with a better understanding of what makes a watch tick.
Today, I still like to take things apart to better understand them. It's a practice I would also encourage you to do.
Let's start by looking at reduce
externally. Right away I can identify 4 parts. The array
, the method
, the reducer
and an initialValue
.
const items = [ 1, 2, 3, 4 ]
const initialValue = 0
const reducer = (accumulator, currentValue) => accumulator + currentValue
items.reduce(reducer, initialValue) //=> 10
/* \ \ \ \
array \ \ - initial value
method \
reducer
*/
Everything is pretty self explanatory. Everything except for the reducer
. This requires a further breakdown.
Note: Reducers have 4 parameters, for now we will ignore the last 2 and focus on the accumulator
and currentValue
.
These parameters are typically abbreviated as acc
and cur
.
const reducer = (acc, cur) => acc + cur
Because you are already familiar with for loops, I can use the for loop below to help demonstrate what the accumulator
and currentValue
are and how they are used.
const items = [ 1, 2, 3, 4 ]
let acc = 0
// \
// initial value
for (let i = 0; i < items.length; i++) {
const cur = items[i]
// \
// current value
acc = acc + cur
// \
// update the accumulator
}
And to insert the reducer
...
for (let i = 0; i < items.length; i++) {
const cur = items[i]
acc = reducer(acc, cur)
}
If you want to see more breakdowns like this, check out Map, Filter, Reduce vs For Loops (syntax).
The Accumulator
In the example above, the accumulator
is a Number
, but it doesn't have to be a Number
, it can be any type.
In this example, acc
is an Array
and the reducer
pushes a doubled value into the accumulator
.
const items = [ 1, 2, 3, 4 ]
const reducer = (acc, cur) => {
acc.push(cur * 2)
return acc
/* \
The reducer must always return the accumulator
*/
}
let acc = []
for (let i = 0; i < items.length; i++) {
const cur = items[i]
acc = reducer(acc, cur)
}
acc //=> [ 2, 4, 6, 8 ]
In this example, the accumulator
is an object and new values are added to the object.
const items = [ 1, 2, 3, 4 ]
const reducer = (acc, cur) => {
acc[cur] = cur * 2
return acc
}
let acc = {}
for (let i = 0; i < items.length; i++) {
const cur = items[i]
acc = reducer(acc, cur)
}
acc //=> { 1:2, 2:4, 3:6, 4:8 }
You should notice between these examples, the for loop code was identical. Don't believe me? Go ahead scroll back and check! Only the initialValue
and the reducer
changed. So whether the accumulator
is a Number
, an Array
, an Object
, or some other type... You only need to change the initialValue
and the reducer
, not the loop!
Reduce
Because we know the for loop never changes, it is easy extract it into it's own function, reduce
.
const reduce = () => {
for (let i = 0; i < items.length; i++) {
const cur = items[i]
acc = reducer(acc, cur)
}
}
Your linter should be complaining about missing reducer
and items
so let's add those. We'll also add an initialValue
while we are at it.
const reduce = (items, reducer, initialValue) => {
let acc = initialValue
for (let i = 0; i < items.length; i++) {
const cur = items[i]
acc = reducer(acc, cur)
}
return acc
}
Is that it? Did we just create reduce
? Seems too simple!
Well, we did ignore those 2 extra parameters in the reducer
. Also, the initialValue
in reduce
should be optional, but it's required in our version. We'll get to that later.
Map
It could be said that map
is a derivative of reduce
. In that case, we can use our reducer
from above, pass this into reduce
and supply an initial value of []
. The initial value is []
because our result will be an Array
.
const map = (items, func) => {
// |
// function to modify value
const initialValue = []
const reducer = (acc, cur) => {
acc.push(func(cur))
// |
// execute func on the currentValue
return acc
}
return reduce(items, reducer, initialValue)
}
const double = x => x * 2
map(items, double) //=> [ 2, 4, 6, 8 ]
Filter
filter
is almost exactly the same as map
. We just have to change the reducer
to filter values based on the results from the predicate
.
const filter = (items, predicate) => {
// |
// if truthy, append to accumulator
const initialValue = []
const reducer = (acc, cur) => {
if (predicate(cur)) {
// |
// run predicate on currentValue
acc.push(cur)
}
return acc
}
return reduce(items, reducer, initialValue)
}
const isEven = x => x % 2 === 0
filter(items, isEven) //=> [ 2, 4 ]
Other Features
The initialValue
in reduce
should be optional. We should be able to do this and get a result of 10
, instead we get NaN
.
const add = (acc, cur) => acc + cur
const items = [ 1, 2, 3, 4 ]
reduce(items, add) //=> NaN
How would you make initialValue
optional? Show off your code in the comments.
I mentioned above that a reducer takes 4 arguments. All 4 arguments are:
- Accumulator (accumulator)
- Current Value (currrentValue)
- Current Index (currentIndex)
- Source Array (source)
We have already implemented the accumulator
and currentValue
. How would you implement currentIndex
and source
? Show me your code in the comments.
Extra Credit
Modify reduce
to work with both an Array
and an Iterator
. This is something Array
's reduce cannot do.
// range is an Iterator.
const range = require('mojiscript/list/range')
const reduce = (items, reducer, initialValue) => {
let acc = initialValue
for (let i = 0; i < items.length; i++) {
const cur = items[i]
acc = reducer(acc, cur)
}
return acc
}
const add = (acc, cur) => acc + cur
// Make this return 10
reduce(range(0)(5), add, 0)
Create a reduceWhile
function. This is just like reduce
, but takes an extra function that will break the iteration when a given condition is met. Think of this as the break
in a for loop.
const predicate = (acc, cur) => acc + cur < 7
const reduce = (items, predicate, reducer, initialValue) => {
/* solution goes here */
}
P.S.
This article ordered the arguments in a specific way to be easier to read for beginners. But if I were to design these functions to be FP friendly, I would order the arguments as such:
- predicate
- reducer
- initialValue
- list
Summary
After deconstructing map
, filter
, and reduce
to learn their inner secrets they become so much more accessible to us.
It's easy to see that by building your own reduce
, you can expand on the features like being able to support an Iterator
or break early. I have gone even further with MojiScript's reduce
by supporting an async Iterator
as well as an async reducer
.
Was there something you'd like me to go into more detail? Did you learn something by reading this article? Let me know in the comments!
If you love Functional JavaScript, follow me here or on Twitter @joelnet!