Map, Filter, Reduce - Tame your loop monsters!
Map, Filter, Reduce
What are Map, Filter and Reduce?
They are functions that let us work with our data in a more expressive way. They are primitives of functional programming, primitives for operations on arrays and objects. Like we have +, -, /, * as basic operations for numbers, we have map, filter and reduce as basic operations for manipulating data.
Map
Map is a simple and very flexible operation. It transforms each element of an array into a new element using the provided function.
Imagine if we had a list of users like this:
let users = [
{ name: 'The Doctor', planet: 'Gallifrey' },
{ name: 'The Master', planet: 'Gallifrey' },
{ name: 'Clara', planet: 'Earth' }
]
and we wanted to get an array of names and home planets, so it would be 'Doctor from Gallifrey', and 'Clara from Earth'.
Using a for loop we could write it like this:
let transformedUsers = []
for (let i = 0; i < users.length; i++) {
transformedUsers
.push(`${users[i].name} from ${users[i].planet}`)
}
Sure, it might not seem like too much overhead to figure out what's going on here. But look at the same functionality refactored to use map
:
let transformedUsers = users
.map(user => `${user.name} from ${user.planet}`)
You have less code clutter - the repetitive code from the loop that we always have to write, but does nothing to help us better understand it.
How does map
work? It's quite simple, we can implement our own in just a few lines:
let map = function (array, callback) {
let newArray = []
for (let i = 0; i < array.length; i++) {
newArray.push(callback(array[i]))
}
return newArray
};
let planetNames = map(planets, function (planet) {
return planet.name
})
Filter
Filter is another simple operation that we can do on our arrays. It filters the array. It goes through every element, checks if it should be included in the resulting array by executing the predicate function. If the function returns true, it will be included, and if it returns false then it won't be.
Imagine we wanted to filter only the users from the Earth. Using a for loop we could write it as:
let usersFromEarth = []
for (let i = 0; i < users.length; i++) {
if (users[i].planet === 'Earth') {
usersFromEarth.push(users[i])
}
}
Simple enough. But look at this same code using filter
:
let usersFromEarth = users
.filter(user => user.planet === 'Earth')
It abstracts the looping away from us, so we can focus on WHAT we want to do, not the HOW.
Also, a nice thing would be to create a function to filter users from any planet like this:
let getUsersFromPlanet = (planet, users) => users
.filter(user => user.planet === planet)
let usersFromEarth = getUsersFromPlanet('Earth', users)
And from that function we can create a function to get users from Earth like this:
let getUsersFromEarth = getUsersFromPlanet
.bind(null, 'Earth')
let usersFromEarth = getUsersFromEarth(users)
That's called currying and it's very powerful.
How does filter
work? Let's find out by examining Mozilla's poylifill:
let filter = function (array, callback) {
let filtered_array = [];
array.forEach(function (element, index, array) {
if (callback(element, index, array)) {
filtered_array.push(element);
}
});
return filtered_array;
};
Reduce
In principle, reduce takes an array and reduces it to a single value by using the provided function. Say we wanted to get a sum of an array. We could do it using a for loop like this:
let planets = [
{ name: 'Mercury', moons: 0 },
{ name: 'Venus', moons: 0 },
{ name: 'Earth', moons: 1 },
{ name: 'Mars', moons: 2 },
{ name: 'Jupiter', moons: 67 },
{ name: 'Saturn', moons: 62 },
{ name: 'Uranus', moons: 27 },
{ name: 'Neptune', moons: 14 }
]
let moonsTotal = 0
for (let i = 0; i < planets.length; i++) {
moonsTotal += planets[i].moons
}
And here's the same thing written using reduce
:
let moonsTotal = planets
.reduce((sum, planet) => sum + planet.moons, 0)
But reduce
let us do other things too, not just reducing arrays to a single value. For example, we can accumulate values in an array or an object. This can be useful when counting occurences of elements in an array. Say we wanted to count how many planets had even and odd number of moons:
let planets = [
{ name: 'Mercury', moons: 0 },
{ name: 'Venus', moons: 0 },
{ name: 'Earth', moons: 1 },
{ name: 'Mars', moons: 2 },
{ name: 'Jupiter', moons: 67 },
{ name: 'Saturn', moons: 62 },
{ name: 'Uranus', moons: 27 },
{ name: 'Neptune', moons: 14 }
}]
let planetEvenOddStats = planets
.reduce((stats, planet) => {
if (planet.moons % 2 === 1) {
stats.odd += 1
} else {
stats.even += 1
}
return stats
}, { even: 0, odd: 0 })
We can even simulate map
and filter
using reduce:
// filter
let planetsWithNoMoons = planets
.reduce((result, planet) => {
if (planet.moons === 0) {
result.push(planet)
}
return result
}, [])
// map
let formattedPlanets = planets
.reduce((result, planet) => {
let formattedPlanet = {
name: `I'm ${planet.name}, a nice planet!`,
moons: `I have ${planet.moons} moons!`
}
result.push(formattedPlanet)
return result
}, [])
So in this regard, reduce
can be very similar to just doing a good old loop, but it still has the benefits of being more expressive.
How does it work? Let's see Mozilla's polyfill implementation:
let reduce = function (array, callback, initial) {
let accumulator = initial || 0;
array.forEach(function (element) {
accumulator = callback(accumulator, array[i]);
});
return accumulator;
};
ForEach
But what if you just want to iterate through an array and something to the data? There's a function for that too - forEach
.
planets.forEach(planet => planet.visit())
What is this "data" that we have in our apps?
Often we need to process data - arrays, objects, complicated data structures. For example we might have data like this in our app:
let users = [{
id: 1,
name: 'The Doctor',
race: 'Time Lord',
homePlanet: {
id: 1,
name: 'Gallifrey',
moons: ['Pazithi Gallifreya'],
location: {
constellation: 'Kasterborous',
galacticCoordinates: '10-0-11-00:02'
}
},
}]
And imagine we had a lot of users from different planets, and we wanted to count the number of users from each planet. How would we go about this task? We might first create a list of all planets, and then go through the users and increment the count on the current user's planet. We might end up writing code like this:
let uniquePlanets = []
for (let i = 0; i < users.length; i++) {
let usersPlanet = users[i].homePlanet
// See if we already have that planet, don't want to duplicate!
let planetAlreadyExists = false
let j = 0
while (j < uniquePlanets.length && !planetAlreadyExists) {
if (uniquePlanets[j].id === usersPlanet.id) {
planetAlreadyExists = true
}
}
if (!planetAlreadyExists) {
uniquePlanets.push(usersPlanet)
}
}
// Now we have our list of unique planets.
// Let's count users from each planet now!
for (let i = 0; i < users.length; i++) {
let usersPlanet
let j = 0
while (j < uniquePlanets.length && !usersPlanet) {
if (uniquePlanets[j].id === usersPlanet.id) {
usersPlanet = uniquePlanets[j]
}
}
usersPlanet.numberOfUsers = usersPlanet.numberOfUsers || 0
usersPlanet.numberOfUsers += 1
}
for (let i = 0; i < uniquePlanets.length; i++) {
let numUsers = 0
for (let j = 0; j < users.length; j++) {
if (users[i].homePlanet.id === uniquePlanets[i].id) {
numUsers += 1
}
}
uniquePlanets[i].numberOfUsers = numUsers
}
// Now each planet in planets array has a numberOfUsers property, and we can use that data to display it on our page.
That's a lot of code! But more importantly, when someone else start reading it, it's not obvious what the intent was. It's not easy to decipher what the code is doing just by looking at it. You need to dive deep into it to understand its purpose.
That's because we don't write what we are doing, we write how we're doing it. It's imperative code, but we should really be striving to produce declarative code as much as possible. Why? Because declarative code is easier to read and understand and also refactor. You write the code only once, but it gets read over and over again. So it's important for it to be as easy to read and understand as possible.
Now, how could we refactor this code using map, filter or reduce functions?
// Lets get unique planets
let planets = users
.map(user => user.homePlanet)
.reduce((uniqePlanets, planet) => {
let alreadyExists = uniqePlanets
.filter(uniqePlanet => uniqePlanet.id === planet.id)
.length > 0
if (!alreadyExists) {
uniquePlanets.push(planet)
}
return uniqePlanets
}, [])
// Lets get count of users from each planet
users.forEach(user => {
let usersPlanet = planets
.filter(planet => planet.id === user.homePlanet.id)[0]
if (usersPlanet) {
planet.numberOfUsers += 1
}
})
// Or, event better, we could do
planets.forEach(planet => {
let numberOfUsers = users
.filter(user => user.homePlanet.id === planet.id)
.length
planet.numberOfUsers = numberOfUsers
})
Chaining Map, Filter and Reduce
Source: https://code.tutsplus.com/tutorials/how-to-use-map-filter-reduce-in-javascript--cms-26209
Let's say I want to do the following:
- Collect two days' worth of tasks.
- Convert the task durations to hours, instead of minutes.
- Filter out everything that took two hours or more.
- Sum it all up.
- Multiply the result by a per-hour rate for billing.
- Output a formatted dollar amount.
let monday = [{
'name' : 'Write a tutorial',
'duration' : 180
}, {
'name' : 'Some web development',
'duration' : 120
}];
let tuesday = [{
'name' : 'Keep writing that tutorial',
'duration' : 240
}, {
'name' : 'Some more web development',
'duration' : 180
}, {
'name' : 'A whole lot of nothing',
'duration' : 240
}];
let tasks = [monday, tuesday];
let result = tasks
// Concatenate our 2D array into a single list
.reduce((acc, current) => acc.concat(current))
// Extract the task duration, and convert minutes to hours
.map((task) => task.duration / 60)
// Filter out any task that took less than two hours
.filter((duration) => duration >= 2)
// Multiply each tasks' duration by our hourly rate
.map((duration) => duration * 25)
// Combine the sums into a single dollar amount
.reduce((acc, current) => [(+acc) + (+current)])
// Convert to a "pretty-printed" dollar amount
.map((amount) => '$' + amount.toFixed(2))
// Pull out the only element of the array we got from map
.reduce((formatted_amount) =>formatted_amount);
More ES6 Array Methods
Want more? There are some other methods in ES6 that will make manipulating data easier for you.
every
andsome
Object.keys
andObject.values
find
andfindIndex
includes
reduceRight
Array.of
Even more? Lodash!
- Can pass shorthand objects for
map
,filter
uniq
intersection
anddifference
- ...
Thank you!
Drop an upvote if you liked the article, share if you believe it will be of help to someone. Feel free to ask any questions you have in the comments below.
Amazing header image from
@minnowpond1 has voted on behalf of @minnowpond. If you would like to recieve upvotes from minnowponds team on all your posts, simply FOLLOW @minnowpond.