Map, Filter, Reduce - Tame your loop monsters!

in #programming7 years ago

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:

  1. Collect two days' worth of tasks.
  2. Convert the task durations to hours, instead of minutes.
  3. Filter out everything that took two hours or more.
  4. Sum it all up.
  5. Multiply the result by a per-hour rate for billing.
  6. 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 and some
  • Object.keys and Object.values
  • find and findIndex
  • includes
  • reduceRight
  • Array.of

Even more? Lodash!

  • Can pass shorthand objects for map, filter
  • uniq
  • intersection and difference
  • ...

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

Sort:  

@minnowpond1 has voted on behalf of @minnowpond. If you would like to recieve upvotes from minnowponds team on all your posts, simply FOLLOW @minnowpond.

        To receive an upvote send 0.25 SBD to @minnowpond with your posts url as the memo
        To receive an reSteem send 0.75 SBD to @minnowpond with your posts url as the memo
        To receive an upvote and a reSteem send 1.00SBD to @minnowpond with your posts url as the memo