An introduction to higher order functions in JavaScript

The problem

When programming for the web, a big part of what we do is marshalling data around our systems. We transform it in places to shape this data in ways that are compatible with both our user interface components and data storage structures. When transforming data, developers will often turn to for and while loops. These language constructs allow us to iterate over collections, mutating data as we go.

This approach can get the job done, but it suffers from several drawbacks. The first of these is mutation. Given that loops do not return anything, to have any value to the outside system they rely on mutating data that lives outside of the loop's block scope. Increasing the scope and volume of code affected during a data transformation increases the cognitive load. What if there was a way to restrict the scope of code affected by a data transformation to only it's block scope and return value? Wouldn't this be easier to understand?

Another issue often seen with this approach is the number of things happening at once tends to increase as the data transformation is built up. Additional layers of nested loops and conditionals with yet more mutation and temporary state will only serve to make the code more difficult to reason about. This ultimately serves to make it harder to change this code with confidence.

Following a functional style with higher order functions provides an alternate way to get the job done, and can alleviate the pain points described previously.

What is a higher order function?

A higher order function is simply a function that takes another function as an argument. This is possible in JavaScript thanks to it supporting first class functions, i.e. the ability to give functions as arguments to other functions. There are many great utility libraries in JavaScript that provide an array of useful higher order functions, however there are also a few higher order functions built into the language. Using only a small handful of these, we're able to quickly build up powerful data transformations that are easy to reason about. In this article, I will cover just three of these - filter, map and reduce.

Filter

Filter is the easiest of these to understand, simply filtering an array of data based on a predicate. This is great for instances when you only care about data within a collection that meets a certain condition.

Given we have an array of users, to filter the array to return only users that are 26, we can do the following:

const users = [  
  {
    name: 'Tom',
    age: 26,
  },
  {
    name: 'Harry',
    age: 27,
  },
  {
    name: 'Sarah',
    age: 26,
  },
];

const usersThatAre26 = users.filter(user => user.age === 26);  

Conversely, to return users that are not 26, simply reverse the predicate:

const usersThatAreNot26 = users.filter(user => user.age !== 26);  

As you can see, we are calling the filter method of the users array and passing a function as the only parameter. This function will be called iteratively on each item in the array, returning a boolean value. Where the boolean value returned is true, the item will be added to the new array that's returned by the filter call. Simple, yet powerful, as well as pure, since the users array itself is not modified.

Map

Unlike filter, map preserves the top level shape of the data, applying a function to each element within the array and returning the transformed array. Map is a great alternative to a loop for applying a transformation to each element within an array using a functional style.

Taking the array of users from before, let's return a new array that contains only the user's names:

const usersNames = users.map(user => user.name);  

Reduce

Reduce is similar to map only much more powerful, thanks to it's ability to change the top level shape of the data. For example, let's say we now want an object containing the users with each user keyed by their name. Using map this isn't possible as the top level shape is an array with three items, however using reduce we can change that top level shape to be an object with three keys. Here's how:

const keyedUsers = users.reduce((acc, user) => (  
  Object.assign(
    {},
    acc,
    {
      [user.name]: {
        age: user.age,
      },
    }
  )
), {});

At first inspection there appears to be a lot more going on here when compared to map, but it's simple really once you get your head around it. The main difference between map and reduce in terms of syntax is the addition of an accumulator. The initial value of the accumulator is provided as the second parameter to reduce, and the value returned from each iteration of the loop's callback function is assigned to the value of the accumulator. This value is passed back into each iteration of the loop as the first parameter to the callback function. The current array value is passed as the second.

To further demonstrate how this works, consider the following implementation of map using reduce:

const mapUsingReduce = (data, callbackFn) => (  
  data.reduce((acc, value) => (
    acc.concat(callbackFn(value))
  ), [])
);

Then, to re-implement the user's names mapping from earlier:

const usersNames = mapUsingReduce(users, user => user.name);  

Conclusion

Hopefully this article has given you a basic understanding of what higher order functions are and why they're useful, with enough knowledge to go out and use filter, map and reduce in the wild. I recommend trying these out in your code next time you get a chance to gain a better understanding.

To compose these higher order functions, simply chain them together. This gets a little tricky when you come to composing these with your own functions, however we can get around this limitation by handrolling our own compose function along with curried versions of filter, map and reduce. As an exercise, why not try creating these yourself? I've created the unit tests for you over on Github here - simply clone the repo and follow the readme to give it a go. If you get stuck, I've provided example implementations too. Good luck!