Over the past couple of months I've started to become a little bit more serious in applying functional programming techniques to my favorite client side language, JavaScript. In particular I've discovered the joy of using Map and Reduce.
These functions (in conjunction with other classic favorites such as .filter
, .sort
etc) and ES6 have made my code more expressive and easier to understand. I'd like to share some of the common map and reduce patterns I've come to use frequently in this blogpost, contrasting these techniques with the corresponding traditional imperative approach.
What are Map / Reduce Good For
Generally speaking, I turn to the map
function if I need to transform some Array
of one object type into another Array
of a different object type. Likewise, I use the reduce
function if I find it necessary to take an array of objects and boil it down to a non-array structure (either a primitive or some JSON object). A large portion of the rest of this blogpost are examples of variations on these two themes.
Why I Prefer Map / Reduce to For Loops
Its possible to achieve the transformations I mentioned above with traditional imperative techniques, largely involving for
loops and mutable data structures.
However there are some advantages to using Map / Reduce over these imperative techniques:
- Less code to wade through and maintain
- Elimination of the use of mutable data structures and variables which leave room for unintended side effects
- Encourages cleaner separation of concerns
I am hoping these benefits will become evident as I explore the map and reduce patterns below.
Summation (reduce)
The simplest application of reduce
I can think of is summing up an array of numbers:
const total = arrayOfInt.reduce((ac, next) => ac + next, 0)
Contrast this against a traditional for
loop approach:
let total
for (let i = 0; i < arrayOfInt.length; i++) {
total += arrayOfInt[i]
}
By using reduce
the code is less verbose, and we avoid having to declare a mutable total variable.
Indexing an array of objects (reduce)
Another common usage of reduce is to turn an array of objects into a single object indexed by some property of the object. In this case, we may have a simple array like this:
[
{
"id": "aron",
"hobby": "games"
},
{
"id": "scott",
"hobby": "ninjitsu"
}
]
... and we wish to represent this array as an object that looks like this:
{
"aron": {
"id": "aron",
"hobby": "games"
},
"scott": {
"id": "scott",
"hobby": "ninjitsu"
}
}
Here is how to do it via .reduce
, the spread operator, and computed properties:
const indexedPeople = peopleArray.reduce((ac, p) => ({ ...ac, [p.id]: p }), {})
This powerful little statement is doing the following:
- For each person
p
inpeopleArray
find theirid
- Make a copy of the
ac
map where there is a value whose key is theid
from (1) and value is the person - When iterating through the array is complete, set the latest incarnation of
ac
map to the invariantindexedPeople
variable
Contrast the above code with this more traditional approach:
const indexedPeople = {}
for (let i = 0; i < peopleArray.length; i++) {
const p = peopleArray[i]
indexedPeople[p.id] = p
}
The functional approach using reduce
accomplishes the goal with a single statement, which leaves no room for accidentally mutating state. This is not the case in the traditional approach, where i
and indexedPeople
could be inappropriately manipulated in some way to inadvertently introduce bugs, once more code (and complexity) is introduced inside of the loop.
Counting occurrences of items in an array (reduce)
Another useful application of the same ingredients (reduce
, spread operator, and computed properties) is to create an object which counts the frequency of items within an array based on some key.
Given this input array:
[
{
"age": 33,
"hobby": "games"
},
{
"age": 28,
"hobby": "chess"
},
{
"age": 21,
"hobby": "ninjitsu"
},
{
"age": 21,
"hobby": "games"
},
{
"age": 21,
"hobby": "kick the can"
}
]
... we can use reduce to produce an object which lists the frequency of each hobby:
{
"games": 2,
"chess": 1,
"ninjitsu": 1,
"kick the can": 1
}
This can be accomplished as follows:
const accumulatedTotals = peopleArray.reduce(
(totals, p) => ({ ...totals, [p.hobby]: (totals[p.hobby] || 0) + 1 }),
{},
)
... which achieves it in the following manner:
- For each person
p
inpeopleArray
, and find their hobby - Find the current total in the
totals
map for that hobby - Create a new copy of the
totals
map where the count hobby's current total is incremented by 1 - When iterating through the array is complete, set the latest incarnation of
totals
map to the invariantaccumulatedTotals
variable
Like the previous indexing example, the functional approach seems less verbose and error prone than the traditional approach:
const accumulatedTotals = {}
for (let i = 0; i < peopleArray.length; i++) {
const p = peopleArray[i]
accumulatedTotals[p.hobby] = (accumulatedTotals[p.hobby] || 0) + 1
}
Flattening an object (map)
Until ES7 comes out with Object.values(...)
, a common way of converting an object's values into an array of values is to do so with map
:
const peopleArray = Object.keys(indexedPeople).map((k) => indexedPeople[k])
Here we are creating a new array of people
objects based on the keys of the indexedPeople
map.
Here is the verbose imperative code for contrast:
const peopleArray = []
const keys = Object.keys(indexedPeople)
for ( let i = 0; i < keys.length; i++) {
peopleArray.push( indexedPeople[ keys[i] ) )
}
Operation chaining encourages separation of concerns
In the following example, we are trying to convert an indexedPeople
object into an array of people sorted by their age, excluding those under 21 years old.
Here's the map
way of doing it
const peopleArray = Object.keys(indexedPeople)
.map((k) => indexedPeople[k])
.sort((a, b) => a.age - b.age)
.filter((person) => person.age >= 21)
Contrasted with the traditional approach:
const peopleArray = []
const keys = Object.keys(indexedPeople)
for ( let i = 0; i < keys.length; i++) {
const person = indexedPeople[ keys[i]
if (person.age >= 21 ) {
peopleArray.push(person) )
}
}
// sort algorithm
const sort(a) => { ... does sorting ... }
sort(peopleArray)
An important thing to note is that the imperative example mixes the flattening and filtering operations in the same 'stage' of the computation. In contrast, the functional paradigm using map
has more of a 'pipeline' approach, where the flattening, sorting, and filtering operations are more clearly discerned as separate steps.
This functional "pipeline" approach makes some operations easier to compose from smaller parts, which are easier to read, test, and maintain. The imperative approach on the other hand has fewer guard rails against bloating the individual pieces, and may lead some developers to make one piece do too much (as illustrated above).
Final thoughts
These aren't by any stretch of the imagination the only applications of map / reduce, but I hope that it is a helpful starting point for developing your own usage patterns.