joepie91's Ramblings

home RSS

Functional programming in Javascript: map, filter and reduce

04 May 2015

This article is meant to be an introduction to functional programming in Javascript - specifically, it will explain the map, filter and reduce methods.

While these are natively available in any recent browser and in Node.js, most articles on them are far too technical to understand, while the concept of these functions is actually really simple, and will benefit any developer - even those working on simple scripts.

Note that I'll be pretty verbose in this article, to make the concept understandable to developers of any skill level. If you're already familiar with some of the concepts described, you can safely skip over those bits. Similarly, if you're a more advanced developer, and examples alone are enough for you, just skip over the text entirely.

I won't address other aspects of functional programming here - these three functions fit well into most workflows on their own. I might explain other concepts in a later post.

So let's start with map, arguably the most common one in a typical project.

So, what is this map business, then?

Think of map as a "for each" loop, that is specifically for transforming values - one input value corresponds to one 'transformed' output value.

You may have found yourself writing code that looks something like this:

var numbers = [1, 2, 3, 4];
var newNumbers = [];

for(var i = 0; i < numbers.length; i++) {
    newNumbers[i] = numbers[i] * 2;
}

console.log("The doubled numbers are", newNumbers); // [2, 4, 6, 8]

While this code works, it's not very nice. It takes a lot of work to do a pretty simple task - double all the numbers in an array.

What if we could simply write our intention of doubling every number in an array?

var numbers = [1, 2, 3, 4];

var newNumbers = numbers.map(function(number){
    return number * 2;
});

console.log("The doubled numbers are", newNumbers); // [2, 4, 6, 8]

Suddenly, we don't need a loop anymore, and we don't have to manually add numbers to an array either! We can simply define our intention, and let map do the work. This can also be chained easily:

var numbers = [1, 2, 3, 4];

var newNumbers = numbers.map(function(number){
    return number * 2;
}).map(function(number){
    return number + 1;
});

console.log("The doubled and incremented numbers are", newNumbers); // [3, 5, 7, 9]

Every number is now doubled, and has 1 added to it. We don't have to manually manage any arrays - we simply specify functions that do some kind of transformation on a single value.

There are a few 'rules' for map functions, though. While these are not always strictly enforced, you should keep to them regardless - they make it a lot easier to understand what your code is doing. If you think these rules will get in your way, just keep reading - your questions will be answered.

The amount of input elements is equal to the amount of output elements

You can't give map 4 values, and only receive 3 back. If the 'source array' has an X amount of elements, then the resulting aray will also have X elements. Every output element corresponds to the input element in the same position - they're never shuffled around.

Your callbacks shouldn't 'mutate' values

What this means, is really just that you shouldn't modify objects or arrays directly from within your callbacks - if the input value is an object or an array, clone it instead, and modify the copy.

This way, there's a guarantee that your callback doesn't cause 'side effects' - that is, no matter what happens in your callback, it will only affect the specific value you're working with. This makes it much easier to write reliable code.

You can clone an array in Javascript by doing array.slice(0). Note that this is a 'shallow clone' - if the values in the array are themselves arrays or objects, they will still be the same values in both arrays.

Shallow-cloning an object is a little more complex. If you're using a CommonJS environment (Node.js, Webpack, Browserify, ...), you can simply use the xtend module. Otherwise, using a function like this should suffice:

function cloneObject(obj) {
    var newObj = {};

    for (var key in obj) {
        newObj[key] = obj[key];
    }

    return newObj;
}

var clonedObject = cloneObject(originalObject);

There is an exception to this rule, but we'll get to that later - just assume that this rule always applies, unless told otherwise.

Don't cause side-effects!

You should never do anything in a map call that modifies 'state' elsewhere. For example, while making a HTTP GET request is fine (although not really possible with plain Array.map), changing another array outside of the callback is not. Your callback can only modify the new value you're returning from that callback.

But... isn't this slow? All those callbacks and the cloning...

Don't worry about it. Such operations are actually rather fast in Javascript, especially in V8-based engines like Node.js, and it's extremely unlikely that it'll ever cause performance issues for you.

Always write code for readability first, and for performance second - it's much easier to optimize readable code, than it is to make optimized code readable.

But what if I only want to transform some of the values?

Perhaps your source array has some values that you want to transform, and some values that you just want to throw away entirely. That's not possible with map alone, as the number of input values and the number of output values for a map call is always equal.

Say you want to double the odd numbers, but throw away the even numbers. Your code may look something like this:

var numbers = [1, 2, 3, 4];
var newNumbers = [];

for(var i = 0; i < numbers.length; i++) {
    if(numbers[i] % 2 !== 0) {
        newNumbers[i] = numbers[i] * 2;
    }
}

console.log("The doubled numbers are", newNumbers); // [2, 6]

This is what it'd look like using map and filter:

var numbers = [1, 2, 3, 4];

var newNumbers = numbers.filter(function(number){
    return (number % 2 !== 0);
}).map(function(number){
    return number * 2;
});

console.log("The doubled numbers are", newNumbers); // [2, 6]

The filter callback is subject to the same "don't mutate" and "don't cause side-effects" rules as map, but the mechanics are different.

The return value from the filter callback should be a boolean, indicating whether to include the original value in the result (true) or whether to leave it out (false). You should not return the value itself, just a boolean - you can't modify the value from within the callback. The return value just decides whether the value will be included in the result, nothing else.

Chaining

As you might have noticed, you can chain map and filter calls indefinitely. Each of them just returns an array, and every array has these methods by default, so you can build a jQuery-like chain. There is no limit to how many of these calls you can chain, and it's usually pretty simple to build complex array transformations from just these functions alone.

An array goes in, an array comes out, and the callback operates on each value individually.

So... what if I just want to combine the values in an array?

'Combining' the values can mean a lot of different things. For example, you might want to sum all the numbers, doubled:

var numbers = [1, 2, 3, 4];
var totalNumber = 0;

for(var i = 0; i < numbers.length; i++) {
    totalNumber += numbers[i] * 2;
}

console.log("The total number is", totalNumber); // 20

But what if we want to do that in a map/filter chain? Easy enough, with reduce:

var numbers = [1, 2, 3, 4];

var totalNumber = numbers.map(function(number){
    return number * 2;
}).reduce(function(total, number){
    return total + number;
}, 0);

console.log("The total number is", totalNumber); // 20

It works like this:

  1. The second argument to the function call is considered to be the 'starting value' for the total - this is what you start out with.
  2. For each item in the array, it calls the callback, with the total value up to that point, and the item itself. For the first item, the 'total value' is the starting value.
  3. You return a new 'total value'. In this case, it's the sum of all previous numbers plus the current number. This return value is used as the 'total value' for the next item.
  4. After running out of items, the 'cumulative' total value is returned.

Again, the "don't mutate" and "don't cause side-effects" rules apply. Otherwise, it doesn't matter what kind of value you're working with - you could be summing a number, concatenating a string, putting together an object, and so on.

Just keep in mind that reduce always returns just the 'total value' - unless you've specifically made it an array, it won't be an array like for map and filter. That means that unless you're explicitly returning an array from it, you can't chain off it any further. But then again, it wouldn't really make sense to run map on a number!

But what if I want to add items? Like filter, but the opposite.

If you use Node.js, you may be familiar with transform streams - these are kind of like a map callback, but besides letting you 'push' (return) 0 or 1 values, they also let you push multiple values. This can be useful if you are, for example, 'flattening' an array of arrays down to a single array with all the values.

While this can't be done with map - after all, one input element means one output element - it can be done with reduce, despite what the name implies. It's a slightly unusual trick, but you'll occasionally find yourself needing it.

Let's say you want to add the even numbers to the resulting array twice, but the odd numbers only once. This is what it could look like:

var numbers = [1, 2, 3, 4];
var newNumbers = [];

for(var i = 0; i < numbers.length; i++) {
    var number = numbers[i];
    newNumbers.push(number);

    if(number % 2 == 0) {
        /* Add it a second time. */
        newNumbers.push(number);
    }
}

console.log("The final numbers are", newNumbers); // [1, 2, 2, 3, 4, 4]

This is how you'd do it with reduce:

var numbers = [1, 2, 3, 4];

var newNumbers = numbers.reduce(function(newArray, number){
    newArray.push(number);

    if(number % 2 == 0) {
        /* Add it a second time. */
        newArray.push(number);
    }

    return newArray; /* This is important! */
}, []);

console.log("The final numbers are", newNumbers); // [1, 2, 2, 3, 4, 4]

Note how we start out with [] as initial value, and just add the value to it one or more times. While this does violate the "no mutation" rule, it's okay in this particular situation - the array object is created explicitly for this reduce call, so it can't cause side-effects anywhere else in the code.

When you use this trick, don't forget to return the array! It's easy to forget this, and you'll end up with an unexpected output. The reduce callback always expects the new total value to be returned, even if that value is an array, and even if it's the same one as before!

The same trick also works with objects - however, in that case you'd specify {} as starting value, and you'd use property assignment rather than .push.

Bonus: forEach

Perhaps you really do want a 'for each' loop - you're not transforming and returning any values, you just want to cause some kind of side-effect (eg. creating a DOM element). It would be nice if you could chain it at the end of a map/reduce/filter 'pipeline', and indeed you can!

This is what your for loop might normally look like:

var numbers = [1, 2, 3, 4];

for(var i = 0; i < numbers.length; i++) {
    doSomethingWith(numbers[i]);
}

And this is what it would look like using forEach:

var numbers = [1, 2, 3, 4];

numbers.forEach(function(number){
    doSomethingWith(number);
});

You can do the same for an object, using Object.keys:

var numbers = {one: 1, two: 2, three: 3, four: 4};

Object.keys(numbers).forEach(function(key){
    var value = numbers[key];
    doSomethingWith(value);
    /* For example, key == "one" and value == 1 */
});

You should really always consider using forEach instead of a for loop . Because forEach uses callbacks, it prevents scope problems with asynchronous operations in a loop - that is, you don't have to worry that the value of the key variable changes inbetween the start and the completion of your operations.

Indexes

At some point, you may want to get the index of the current item in the source array - however, I've only demonstrated the use of the value so far.

Getting the index of the current element is trivial: it's the second argument for map, filter and forEach, and the third argument for reduce. In other words, it comes after the arguments I've shown in the examples so far.

If you need a reference to the original array for some reason, that is the argument after that - however, needing this usually means that you're doing something wrong. You should never directly modify the source array from any of these functions, for example.

Further reference

MDN has further documentation on map, filter, reduce, and forEach.

Other libraries

Some utility libraries like lodash and underscore offer similar methods, but you almost certainly don't need them. All modern Javascript runtimes and browsers have these functions available natively on array objects.

jQuery offers similar methods, but keep in mind that the jQuery equivalents switch around the value and the index - that is, the order of arguments for the callback is (index, value). A more reliable way is to use this within your callback where possible - it is always set to the value in jQuery.

My next Javascript and Node.js article will be about how Promises can help you escape callback hell. I will be discussing the bluebird library specifically, including its equivalents of map, filter, reduce and forEach (named each in bluebird).

I am currently offering Node.js code review and tutoring services, at affordable rates. More details can be found here.