loading...

Explain it to me like I'm five: .map, .reduce, & .filter edition

rcmaples profile image RC Maples ポ1 min read

I'm having trouble understanding how to use map, reduce, and filter to iterate over an array (or an array of objects for that matter 🙄).

I generally use for loops (and nested for loops if needed), but would really like to switch over to map, reduce, and filter for various things. I just can't wrap my head around how it works and what it's doing.

Here's a sample bit of code where I think I could use map/reduce/filter to achieve the same results a bit cleaner.

const jsIngredients = [
    {"ingredient-1":"chicken"},
    {"ingredient-2":"brocolli"},
    {"ingredient-3":"cheese"}
];

let ingredientString = "";

for (let k = 0; k<jsIngredients.length; k++) { 
    if (jsIngredients[k].value) { // if non-empty
        ingredientString +=  `${jsIngredients[k].value},`;
        // ingredientString = "chicken,brocolli,cheese," 
        }
    }
ingredientString = ingredientString.slice(0,ingredientString.length-1);
// ingredientString = "chicken,brocolli,cheese" 

Any help?
🍻

Discussion

markdown guide
 

img

I can't take credit for the image, but it seemed to helped me to remember how to use each 😉

 
 

This is awesome, do you have the source for this image?

 

I'm not sure of the original source, but I know I've seen it circulating on twitter a few times.

 
 
 

Each of map, filter, and reduce steps through an array element by element and does something with each element, where 'something' is defined by the callback function you pass.

map applies a transformation to the element, so its callback function just takes the element itself (there are extra arguments, like the current index, in case you need them). So map's output is an array just as long as the original array, but where each element has been transformed from the original.

filter accumulates only those elements for which the callback function returns true. Like map, the callback operates on the original element with the same extra arguments, but it has to return a boolean-ish value. filter's output is an array which is either shorter or the same length as the original, and which contains only those elements for which the callback function returns a truthy value.

reduce works on a separate value which accumulates changes from each array element. Its callback is a little different: the first argument is the accumulator, then the element, then the extra arguments. It has to return the accumulator once it's been updated (or even if it hasn't -- filter + reduce is a waste of time, just use an if in the reduce callback!). You can use reduce to transform an array into something else entirely, like you're trying to do here.

There is a simple two-step solution involving map and join, but reduce will do it in one. I'll leave implementing the callback to you, but you're looking at const ingredientString = jsIngredients.reduce((accumulator, ingredient) => {...}, '');.

Also, doublecheck your original array -- based on your loop, the ingredient-x key should just be a consistent value instead.

 

Imagine you have small machines attached to arrays that take a function and use the array to do stuff with it.

  • map: this will take each item of the array it is attached to, runs it through the function and will return a new array with the results:

    [1, 2, 3].map(x => x + 1) // [2, 3, 4]
    
  • reduce: very much the same as map, but instead of an array, it will provide the function with the result of the last operation (or at the start the second argument it received) and returns the single last result instead of an array:

    [1, 2, 3].reduce((r, x) => r + x, 0) // 6
    
  • filter: this will return an array of all the values of the array it was attached to that had the function return a true-ish result:

    [1, 2, 3].filter(x => x % 2 === 1) // [1, 3]
    
 

Actually, the best thing about this question is no sane 5yo would ever consider explicitly looping through a collection to find things (filter), or perform the same calculation over each element (map), or run a calc over all the elements and produce a single result (reduce) :-)

A tendency towards naturally writing a loop to iterate over a collection is a natural side effect of imperative programming experience, and it's one of the first mental pain points you feel when you write using the functional programming approach of map/filter/reduce (along with others). I can recall the same "where are my loops?" response when first crossing from Java (imperative) to Clojure (functional).

I like to think of these functions as hiding all the boilerplate code you write from common patterns of list processing and letting the author focus on the main thing that changes: the expression you need to map/filter/reduce. If you use an explicit loop to find all the positive numbers in a collection of numbers and then a separate loop to find all the negative numbers, then the only code that changes between these two solutions should be the expression you use to test whether a number is positive. Everything else is boilerplate, and all the boilerplate is hidden inside those functions.

 

If you have an array of things and what you want is a single thing, reduce is what you need.

It works by using a variable known as the 'accumulator', which is just the result you build up after processing each element in your array. In your case, that'd be ingredientString.

(I should point out at this point that your code as posted doesn't work - jsIngredients[i].value will always be undefined. Give the objects in your array consistent keys, like just ingredient instead of ingredient-1/2/3, then you can access them with jsIngredients[i].ingredient. I'll assume you made this change and carry on)

So, breaking down what reduce will do:

  • You pass reduce two things: a function (the 'reducer' function) which takes the 'accumulator' value and an element of your array, and an initial value for the accumulator
  • reduce will call your reducer function, passing it the initial accumulator value and the first element of your array
  • Your reducer function does whatever you like with the accumulator and your array element. It should eventually return a value, and that value will become the new value of the accumulator.
  • reduce then calls your reducer function again, passing the new accumulator value and the next element of your array.
  • Repeat until no more elements remain.

And in code:

const jsIngredients = [
    {"ingredient":"chicken"},
    {"ingredient":"brocolli"},
    {"ingredient":"cheese"}
]

let ingredientString = jsIngredients.reduce((acc, element) => {
    if (element.ingredient) {
        return acc + `${element.ingredient},`
    }
    return acc
}, '')

ingredientString = ingredientString.slice(0,ingredientString.length-1)
// "chicken,brocolli,cheese"

Note the passing of an empty string to reduce as the initial value. Also note this is a very rough idea of how reduce is typically used, see developer.mozilla.org/en-US/docs/W... for more details.

I also wrote goo.gl/9sQAQw but only ever got around to explaining map, still might be useful.

Finally, rather than writing the ingredient string manually and having to slice out the extra comma, consider how you might adapt this to use Array.prototype.join to solve this for you. 😁

 

Think of it like a car factory, where stuff goes in and cars come out, with long assembly lines all over the place. Along these assembly lines, there are places where stuff goes into a machine, and other stuff comes out at the other end.

Closely observe one of those machines: on the left side, pieces of raw metal go in, and on the right side, for each of those pieces of raw metal, a metal screw comes out. Some of the metal pieces are copper, other are steel, but all come out as the same kind of screw (though in different kinds of metal). In essence, the machine takes in raw metal pieces and transformed them into screw. That machine is map.

There are other machines that take in the screw, and then look at the quality of the screw. It lets pass the ones that are okay, but removes the bad ones (without transforming the screws in any way). You are currently looking a filter machines.

Finally, there are also other machines that take in lots of pieces, and output a single combined piece. It doesn't just lump them together all at once, but rather

  1. starts with a single piece that it's given
  2. takes a second piece and combines it with the first to form a new single piece
  3. takes a third piece and combines that with the newly created piece, to form another newly created piece
  4. repeats this until there are no more pieces to be had
  5. puts the lastly created single piece on the assembly line to continue on

This is a reduce machine.

 

There are already some good comments others have made, but I'll add a small comment of my own. Here are two articles written by @machy44 where he implemented his own versions of map and filter. Maybe thinking about how you'd make your own version of these functions could be helpful:

A simple version of reduce would be implemented in a similar vein.

Here is a version of reduce I wrote for my article on asynchronous generators:

const asyncReduce = async function* (iterable, reducer, accumulator) {
    for await (const item of iterable) {
        const reductionResult = reducer(item, accumulator)

        accumulator = reductionResult

        yield reductionResult
    }
}

A normal synchronous version should be a pretty simple cleanup of the above code:

const syncReduce = function (iterable, reducer, accumulator) {
    for (const item of iterable) {
        const reductionResult = reducer(item, accumulator)

        accumulator = reductionResult
    }

    return accumulator
}

As @andeemarks points out, these functions basically abstract away the for loop boilerplate.

I thought I'd also add that these are not the only ways to do this kind of thing. For example, Python has list comprehensions that I think are often more clear. I don't think JavaScript has them though.

 

Awesome! Thanks for the feedback, sorry my code example is a bit crap. I pulled some live code and then realized some pieces were missing and just kinda slapped something in there. Really appreciate the different approaches to the topic!