Deconstructing Map, Filter, and Reduce

JavaScript Joel - Nov 27 '18 - - Dev Community

Disassembled Camera

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.

Pulp Fiction watch scene with Captain Koons

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!

Cheers!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player