DEV Community

RC Maples
RC Maples

Posted on

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

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" 
Enter fullscreen mode Exit fullscreen mode

Any help?
🍻

Top comments (12)

Collapse
 
tterb profile image
Brett Stevenson

img

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

Collapse
 
evanoman profile image
Evan Oman

This tweet appears to be the original

Collapse
 
sudiukil profile image
Quentin Sonrel

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

Collapse
 
tterb profile image
Brett Stevenson

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

Collapse
 
humzakhan profile image
Humza K.

Woah, this is too good!

Collapse
 
jochemstoel profile image
Jochem Stoel

Haha this is great.

Collapse
 
dmfay profile image
Dian Fay • Edited

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.

Collapse
 
lexlohr profile image
Alex Lohr • Edited

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]
    
Collapse
 
kvsm profile image
Kevin Smith 🏴󠁧󠁢󠁳󠁣󠁴󠁿

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. 😁

Collapse
 
alainvanhout profile image
Alain Van Hout • Edited

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.

Collapse
 
nestedsoftware profile image
Nested Software • Edited

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.

Collapse
 
rcmaples profile image
RC Maples

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!