Functional programming with JavaScript arrays

Learn how JavaScript’s built-in functions like map() and filter(), as well as compose() and chain(), support a more elegant handling of JavaScript arrays.

Functional programming with JavaScript arrays
Thinkstock

JavaScript arrays are an incredibly flexible way to model collections using techniques from functional programming. This article introduces you to using tools like forEach(), map(), and reduce() for functional-style arrays.

Traditional JavaScript arrays

JavaScript’s arrays can hold heterogeneous types, change size on the fly, and readily insert or remove elements. Traditional methods like slice, splice, and push/pop do this by operating on the array itself, modifying the collection in a “destructive” way:


// Create an array with heterogeneous types:
let myArray = [10, "hello", true, { name: "Alice" }];

// Add to an array and change size on the fly:
myArray.push(42);

// Extract elements without modifying the original array:
let extracted = myArray.slice(1, 3);

Functional programming with arrays

Although JavaScript’s arrays are very capable out of the box, the functional paradigm improves the clarity and maintainability of array code. In general, functional programming looks to use functions as operators that can be passed into arrays. This allows for operating over the array like the head on a tape, rather than traditional imperative loops that describe in detail what is to occur.

Let's look at a few examples of working with arrays in the functional paradigm.

forEach()

Array.forEach() is our first example. This lets you pass in a function that performs arbitrary operations on the elements iteratively. It’s a common alternative to the traditional for loop:


myArray.forEach((element) => {
  console.log("my element is: " + element);
})

In this example, we are just outputting each element to the console. The equivalent in a for loop would be:


for (let i = 0; i < myArray.length; i++) {
  console.log("my element is: " + myArray[i]);
}

You’ll notice that there are fewer moving parts in the functional version. In particular, we eliminate the iterator (i), which is an extraneous variable used to express the logic of the mechanics, rather than a part of the actual intention. Note that I'm not suggesting for loops have no place; they are sometimes the right tool, but often, forEach() is a cleaner approach.

Functional programming as a philosophy promotes “immutability.” That means simply that it likes to avoid modifying variables. Instead, functional programming prefers to take an existing variable and pass it through a “pipeline” (a function or functions) that transforms it into a new variable, leaving the original as-is.

forEach is often used in this way, but it’s also often used “destructively,” as shown here:


const numbers = [1, 2, 3, 4, 5];

numbers.forEach(function(number, index) {
  if (number % 2 === 0) { // Check for even numbers
    numbers.splice(index, 1); // Remove even numbers from the array
  }
});

This example might not be considered the purest form of functional programming, but it utilizes key functional characteristics such as “first-order functions.” When we refer to a first-order function, we mean that we are using a function like any other reference, in this case, by passing it in as an argument. The long and the short of that story is that functions can act as portable bundles of functionality that are passed around to do jobs in predictable ways.

Note, too, that there are still many cases where an old-fashioned for loop is the best approach. For example, when iterating by a number other than 1, iterating backward, and when handling complex scenarios requiring multiple iterators.

Array.map()

Functions that are non-destructive and avoid any other “side-effects” are said to be “pure functions.” We can use forEach in this way, but the Array.map() function is specifically designed for this purpose. It does not operate on the array itself, but instead runs the function operator and returns the result as a new array:


const bands = [
  { name: "Led Zeppelin", year: 1968 },
  { name: "Pink Floyd", year: 1965 },
  { name: "Queen", year: 1970 },
  { name: "The Clash", year: 1976 },
  { name: "The Ramones", year: 1974 },
  { name: "R.E.M.", year: 1980 }, 
];

const bandNames = bands.map(band => {
    return band.name;
});
// bandNames is an array that has just the string band names

Array.map() is a very powerful mechanism for transforming arrays. It gives you the ability to do almost anything with an array in a clean fashion. In particular, it avoids complexity in changing the original array, where other code elsewhere might depend on it in unknown or unexpected ways. 

On the other hand, it’s important to bear in mind that Array.map() always makes a copy, which has performance implications. You don’t want to use it on very large arrays. Sometimes, memory considerations dictate that you use another approach.

How this works is that whatever the provided function returns will be kept in the new array. So, we could use the automatically returning version of a function:


const bandNames = bands.map(band => band.name)

This approach can be a lot cleaner for short functions.

Array.filter()

Array.map() outputs an array with the same length as the source. If the function doesn’t return something, the output array will be labeled undefined in that position. To create an array with a different length, you can use Array.filter(). In that case, when the functional argument doesn’t return anything, that element will be removed from the target array:


const bands = [
  { name: "Led Zeppelin", year: 1968 },
  { name: "Pink Floyd", year: 1965 },
  { name: "Queen", year: 1970 },
  { name: "The Clash", year: 1976 },
  { name: "The Ramones", year: 1974 },
  { name: "R.E.M.", year: 1980 }, 
];

const seventiesBands = bands.filter(band => {
  if (band.year >= 1970 && band.year < 1980) {
    return band;
  }
});

// seventiesBands is an array holding only those bands satisfying the condition (band.year >= 1970 && band.year < 1980)

In this example, we take an array of objects holding rock bands and the year they were formed and then use bands.filter() to provide a function that will give us a new array holding only the bands from the 1970s. 

Array.reduce()

Sometimes, you need to take a whole array and turn it into a single value. For that, you can use Array.reduce:


// same band array as source

const earliestBand = bands.reduce((earliestSoFar, band) => {
  return band.year < earliestSoFar.year ? band : earliestSoFar;
}, { year: Infinity }); // Start with a band in the infinitely distant future

console.log(earliestBand.name); // outputs “Pink Floyd” 

The function passed to reduce() has two arguments: the “accumulator” and the current element. The accumulator is what will be finally returned, and holds its state across each iteration, allowing you to “collect” everything into a single output.

The reduce function is a very handy tool when you need it. As another quick example, say you wanted a string containing all the band names in a string. You could do this:


const allBandNames = bands.reduce((accumulator, band) => {
  return accumulator + band.name + ", ";
}, ""); // Initial value is an empty string

Composing functions

The built-in functions you’ve seen so far are fundamental to functional programming (and its programming-in-the-large sibling, reactive programming). Now, let's consider the idea of linking functions together to achieve some desired functionality.

Two of the most basic and important linking functions are compose() and chain(). Many functional programming and utility libraries include them, but they are also easy to implement. This next example gives you a clear look at how they work:


const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);

const chain = (...fns) => (xs) => xs.reduce((acc, x) => acc.concat(fns.reduceRight((v, f) => f(v), x)), []);

compose() combines many functions, so that each function's output is fed into the next, from right to left (based on their ordering as passed to the function). chain() does the same thing, but from left to right.

These functions also give you a look at reduceRight(), the mirror image of reduce(), which you’ve already seen. The reduceRight() function lets you accumulate by going backward across the functional arguments.

The compose() and chain() functions are not specific to arrays, but they can be used with them. Here’s a simple example of using compose() with an array:


const numbers = [1, 4, 2, 8, 5, 7];

// Define reusable higher-order functions:
const findEvenNumbers = arr => arr.filter(n => n % 2 === 0);
const doubleNumbers = arr => arr.map(n => n * 2);
const sortNumbers = arr => arr.sort((a, b) => a - b);

// Compose functions to create complex transformations:
const processNumbers = compose(sortNumbers, doubleNumbers, findEvenNumbers);

const processedNumbers = processNumbers(numbers);
console.log(processedNumbers); // Output: [4, 8, 16]

Conclusion

Arranging functions is central to both functional and reactive programming. It allows you to reuse and combine functions into new functions. In essence, you can define composite functions that are composed of the capabilities of other, more focused ones. This is similar, conceptually, to how an object-oriented programmer thinks about composing applications out of objects. 

Because functions express their work in such a minimalist way—with input and output being their entire API surface—they provide an exceptionally clean approach. Of course, as you become more sophisticated, you lose some of this clarity. Even the compose() and chain() functions leave behind some of the elegance of straight functions. 

In general, handling array functions like we’ve seen here, using JavaScript’s built-in functions like map() and filter(), is an excellent application of the power of functional programming.    

Copyright © 2024 IDG Communications, Inc.