Skip to content

Instantly share code, notes, and snippets.

@peetklecha
Created March 5, 2020 20:39
Show Gist options
  • Save peetklecha/81b583a94c8c6f29662029ff927dc1fc to your computer and use it in GitHub Desktop.
Save peetklecha/81b583a94c8c6f29662029ff927dc1fc to your computer and use it in GitHub Desktop.
Sorting out map and filter.

We have now been using array methods for a while, and two in particular have seen a lot of use: .map and .filter. Y'all have mostly been using these successfully, but we've noticed some issues especially with how you're using .filter so I wanted to take some time to clarify. Even if you feel like you understand these methods, read closely -- part of the issue is that you can get away with misusing these methods to some degree without realizing it, only to have it bite you later.

Map

If I call .map on array A, it will return a new array, B, which has the same length as A. The elements of B will be based on the elements of A, according the callback I gave it. In essence I am transforming each element in A into a new element in B, which is why B must have the same length as A. Whatever is returned by the callback you pass into .map is what goes into B, which is why it's important not to forget to return from your .map-callback.

A = [1,2,3]
A.map(x => x+2) => [3,4,5]

I think everyone gets this pretty much. The problem, really, is that people have been oftening treating .filter like it is also .map.

Filter

If I call .filter on array A, it will return a new array B. B can only contain things that were already in A. Unlike .map, .filter cannot transform the elements in A. It can only (as the name suggests) filter things out. So unlike .map, .filter can return a new array which is shorter than the original. Or it might return an exact copy of the same array. But it cannot return anything else: It cannot return a longer array, and it cannot return an array which contains anything not already in A.

This is a big important difference with .map: The value you return from .map's callback is what goes into the new array created by .map. But the value you return from .filter's callback is NOT what goes into the new array created by .filter. The callback you pass into .filter should always return a Boolean; it should not typically return data. This I think is the single biggest point of confusion.

This is a very common misuse of filter we have seen a lot in stretches and homeworks:

const a = [1,2,3,4,5,6,7,8]
const b = a.filter(x => {
	if(x > 5){
		return x
	}
})
//b is [6,7,8]

If the goal is to get b to be the array containing elements of a which are greater than 5, then this does, technically, succeed. But only accidentally. First, look at how we should write this instead:

const b = a.filter(x => {
	if(x > 5){
		return true
	} else return false
})

Of course, since we are returing true when x > 5 === true, and returning false when x > 5 === false, we could simplify this a lot by simply returning x > 5:

const b = a.filter(x => {
	return x > 5
})

And then we can condense this even further by dropping the curlies:

const b = a.filter(x => x > 5)

Now that looks like a filter!

What happens when a.filter(x=> x > 5) is evaluated? Here's the play-by-play:

a => [1,2,3,4,5,6,7,8]
b = []
1: 1 > 5 => false; b = []
2: 2 > 5 => false; b = []
3: 3 > 5 => false; b = []
4: 4 > 5 => false; b = []
5: 5 > 5 => false; b = []
6: 6 > 5 => true; b = [6]
7: 7 > 5 => true; b = [6,7]
8: 8 > 5 => true; b = [6,7,8]
return b

We apply the callback to each element x in the array. If callback(x) returns true, add x to the new array. If it returns false, do nothing.

Again, what the function returns is NOT what is added to the new array -- if it was, we'd have an array full of trues and falses. What's return by the callback instead determines whether the original item is added to the new array.

So here's where things get confusing! Go back to the misused filter:

const a = [1,2,3,4,5,6,7,8]
const b = a.filter(x => {
	if(x > 5){
		return x
	}
})
b => [6,7,8]

This actually works! And here's why: because of coercion. Remember that all numbers (besides 0) are truthy, and all objects are truthy, all strings (besides "") are truthy, etc. So when the condition is met (x >5) you are returning a number (which is truthy, so it is coerced into true). And when the condition isn't met, you are returning nothing -- and remember that when you don't explicitly return something, undefined is implicitly returned, and undefined, when coerced into a boolean, is false!

a => [1,2,3,4,5,6,7,8]
1: undefined ~> false; //b = []
2: undefined ~> false; //b = []
3: undefined ~> false; //b = []
4: undefined ~> false; //b = []
5: undefined ~> false; //b = []
6: 6 ~> true; // b = [6]
7: 7 ~> true; // b = [6,7]
8: 8 ~> true; // b = [6,7,8]
return b

So this does work, but sort of accidentally, because of two facts: our return values happen to all be truthy, and the default return value happens to be falsey. But if one of our numbers was 0, it would not be included in the filter even if it should be! What's more, thinking of the return value as the thing that goes into the new array leads you to think you can specify exactly what goes into the array, e.g., by transforming the object:

a.filter(x => x.name)

Maybe you want this to give you the names of all the elements in a which have names, but it won't: It will just give you the elements in a whose names are truthy. And if that is what you actually want then that's perfectly reasonable. But if what you want is the names of the elements with names, you need to use both .map and .filter.

The most straightforward way to combine them is like so:

a.filter(x => x.name).map(x => x.name)

This first will filter down to just the array of things in a which have the .name property (and have a truthy value for it.) Then it maps those elements to their names.

You could also do a sort of silly thing:

a.map(x => x.name).filter(x => x)

The first .map will produce a new array of the same length as a, where each element of the new array is the name of the corresponding element of a. But if some elements in a don't have a .name property, then the corresponding element in the mapped array will be undefined. The trivial filter will then filter out all falsey elements in the array -- i.e., it will filter out all the undefineds. But remember that you must be sure that falsey elements are always excluded: If some people have empty strings for names, those will be excluded here, which you might not want. Likewise, if you are dealing with numbers, this approach would remove any zeros. Doing .filter first, then .map, is safer.

//I want to get the numbers in a, multiplied by 2, but only if the original number is less than 6.

const a = [-1,0,3,5,6,9]
const b = a.filter(x => x < 6).map(x => x*2) //good, b = [-2,0,6,10]
const c = a.map(x => x < 6 && x * 2).filter(x => x) //bad, b =[-2,6,10]

Because 0 is falsey, it got filtered out inappropriately. Oops!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment