Reducing Filter and Map Down to Reduce
August 14, 2013
.reduce()
method?Not So Common Use Case
If you were anything like me, I first went and looked for some examples of how to use the .reduce()
method. Most of the examples that I ran across all looked the same and didn't really seem very convincing to me. Most of the examples I found were adding up numbers across various objects. Here is the type of code that I saw...
var developers = [
{ name: "Joe", age: 23 },
{ name: "Sue", age: 28 },
{ name: "Jon", age: 32 },
{ name: "Bob", age: 24 }
], age = 0;
age = developers.reduce(function(memo, developer) {
return memo + developer.age; // return previous total plus current age
}, 0); // initialize age with 0 that will be passed as memo
console.log("Sum of all developer ages is " + age);
// Output: Sum of all developer ages is 107
Adding up values across a collection does seem like it could be helpful on occasion, but it didn't seem like it would be a very common use case in my day-to-day development. Deep down somewhere in my inner-algorithm I felt like there must be a better use case for this lowly .reduce()
method. I wanted so much more for it.
Data to Manipulate
So, before we proceed let's take a step back before revisiting the .reduce()
method again. Partly to celebrate the announcement of the 12th Doctor and to make things a little more interesting we are going to use data about the 12 Doctors as we unpack .filter()
, .map()
, and .reduce()
. Here is the data we will be using throughout the rest of this blog post.
var doctors = [
{ number: 1, actor: "William Hartnell", begin: 1963, end: 1966 },
{ number: 2, actor: "Patrick Troughton", begin: 1966, end: 1969 },
{ number: 3, actor: "Jon Pertwee", begin: 1970, end: 1974 },
{ number: 4, actor: "Tom Baker", begin: 1974, end: 1981 },
{ number: 5, actor: "Peter Davison", begin: 1982, end: 1984 },
{ number: 6, actor: "Colin Baker", begin: 1984, end: 1986 },
{ number: 7, actor: "Sylvester McCoy", begin: 1987, end: 1989 },
{ number: 8, actor: "Paul McGann", begin: 1996, end: 1996 },
{ number: 9, actor: "Christopher Eccleston", begin: 2005, end: 2005 },
{ number: 10, actor: "David Tennant", begin: 2005, end: 2010 },
{ number: 11, actor: "Matt Smith", begin: 2010, end: 2013 },
{ number: 12, actor: "Peter Capaldi", begin: 2013, end: 2013 }
];
Problem to Solve
What we want to do is to take all the Doctors from year 2000 until today and change their data up just a little bit. We want to massage the data a little bit and have keys of doctorNumber
, playedBy
, and yearsPlayed
. We don't want to just directly map one field to another, but for doctorNumber
we want to prepend a "#"
and for the yearsPlayed
we want to want how many years the doctor played and not a range.
Desired Output
Based on the above problem that we want to solve, the end result of our data manipulation should produce the following results.
[
{ doctorNumber: "#9", playedBy: "Christopher Eccleston", yearsPlayed: 1 },
{ doctorNumber: "#10", playedBy: "David Tennant", yearsPlayed: 6 },
{ doctorNumber: "#11", playedBy: "Matt Smith", yearsPlayed: 4 },
{ doctorNumber: "#12", playedBy: "Peter Capaldi", yearsPlayed: 1 }
]
So, let's first use the .filter()
and .map()
methods to solve this, and then we will redirect to solve the same problem using the reduce method.
Filter and Map
ECMAScript 5 Array Filter and Map
IE9+ and other modern browsers implement ECMAScript 5 and with that comes a lot of nice features in JavaScript. Some of these nice features are additional array methods. Two of these handy methods are .filter()
and .map()
.
Here we use the .filter()
method to narrow down the array items to only those that include Doctors that only began past the year 2000. The method will iterate over your array of items and invoke the function you provided passing it as an argument. If you return a truthy value from the function then that means you want to keep the item in the resultant array otherwise the item will not be included in the result.
The result of the .filter()
method is an array so we then immediately call the .map()
method off of that array. This method also iterates over the array, much like the .filter()
method did, but the return inside of the function argument for .map()
defines what that object will look like in the new array. In this case we are changing up our object to use new keys and to change up the values slightly.
doctors = doctors.filter(function(doctor) {
return doctor.begin > 2000; // if truthy then keep item
}).map(function(doctor) {
return { // return what new object will look like
doctorNumber: "#" + doctor.number,
playedBy: doctor.actor,
yearsPlayed: doctor.end - doctor.begin + 1
};
});
console.log(JSON.stringify(doctors, null, 4));
ECMAScript 5 Polyfill
If you are using an older browser (IE8 or less) then you won't have the ES5 methods available to you. However, you can still use the above code if you provide a ES5 polyfill to fill in the functionality gap. A polyfill is a library that will mimic a native browser's API if it isn't available. HTML5Please recommends either using es5-shim or augment.js if you want to support ES5 methods.
The nice thing about a polyfill is that if the native feature does exist, then it will be used. The polyfill should only kick in if the browser does not provide the functionality you are expecting. The idea is that your code can be written in such a way that it follows the official browser API and hopefully one day you won't need the polyfill anymore or at least not many people will need it.
Underscore Filter and Map
Instead of using the native ES5 array methods or using a polyfill, you could instead decide to use a library such as Underscore or Lo-Dash. These libraries contain many helper utilities methods that I find helpful in most every web application I write. Two of many methods provided are .filter()
and .map()
.
doctors = _.filter(doctors, function(doctor) {
return doctor.begin > 2000;
});
doctors = _.map(doctors, function(doctor) {
return {
doctorNumber: "#" + doctor.number,
playedBy: doctor.actor,
yearsPlayed: doctor.end - doctor.begin + 1
};
});
console.log(JSON.stringify(doctors, null, 4));
The code is very similar to what we had before. The main difference is that we need to split out our two method calls. This is a little cumbersome, but it makes sense because the methods return an array and we can't chain them together and continue to call Underscore methods.
Underscore Chaining
Thankfully, there is a way in Underscore that we can chain these together if we want to. There is some extra syntax sugar that we need to sprinkle, but it isn't too much.
The main thing you have to do is the call the .chain()
method and pass in the array that you want to use while you chain Underscore methods. From then on you just keep calling Underscore methods as you like and it will share the array among the various methods. At the point you are ready to get the result of your manipulations you can call the .value()
method to get the value.
doctors = _.chain(doctors) // starts chain using the doctors array
.filter(function(doctor) { // uses array from chain
return doctor.begin > 2000;
})
.map(function(doctor) { // uses array from chain
return {
doctorNumber: "#" + doctor.number,
playedBy: doctor.actor,
yearsPlayed: doctor.end - doctor.begin + 1
};
})
.value(); // gets value from chain
console.log(JSON.stringify(doctors, null, 4));
I personally don't use Underscore's chaining mechanism, but it is nice to know that it does support that feature.
Reducing Filter and Map with Reduce
So, back to the main topic of this blog post. After finding the same use case over and over again on the internet (summing up a property over a collection) I then approached some friends of mine to see if they knew of another use case for the .reduce()
method. The infamous Jim Cowart came to the rescue! He actually had written a blog post about this very topic called Underscore – _.reduce().
ECMAScript 5 Reduce
So, let's convert the above .filter()
and .map()
code above and convert it to use the .reduce()
method instead. As the first argument you provide the .reduce()
method a function that will be invoked for each item in he array. In addition this method also takes a second memo
argument that will be passed from one iteration to the next. Instead of passing a number, like we did at the beginning of this post, we are going to pass an empty array. Then inside our function argument we will have an if
statement, which will serve as our "filter", and if our criteria is matched we will push a new value to our memo
array. The "map" happens as we push a custom object onto our array. Before we finish our function we need to return the memo array. Once the statement has completed then a new array will be returned that will be the filtered and mapped version that you wanted.
.reduce()
method.doctors = doctors.reduce(function(memo, doctor) {
if (doctor.begin > 2000) { // this serves as our `filter`
memo.push({ // this serves as our `map`
doctorNumber: "#" + doctor.number,
playedBy: doctor.actor,
yearsPlayed: doctor.end - doctor.begin + 1
});
}
return memo;
}, []);
console.log(JSON.stringify(doctors, null, 4));
Underscore Reduce
Much like our previous examples, you can also use the .reduce()
method from Underscore or Lo-Dash instead of the native or polyfilled version.
doctors = _.reduce(doctors, function(memo, doctor) {
if (doctor.begin > 2000) {
memo.push({
doctorNumber: "#" + doctor.number,
playedBy: doctor.actor,
yearsPlayed: doctor.end - doctor.begin + 1
});
}
return memo;
}, []);
console.log(JSON.stringify(doctors, null, 4));
Other Methods
If you like what you see with .filter()
, .map()
, and .reduce()
then you are in luck because there are many more methods that you can use that are available both in ES5 and also in Underscore or Lo-Dash. Knowing what is available is half the battle ;)
Tweet about this post and have it show up here!