Benefits of Functional Programming by Example
Functional programming is a programming paradigm (technique) where functions are used together to return new values, rather than modifying variable values multiple times. Functions that always return the same result given the same inputs are said to be free of side effects, or pure. When code relies on changes to external variables, it can lose its purity because the same inputs no longer create the same outputs consistently. By finding purer ways to design programs, we can use functions in ways that won’t impact how other code works through side effects. This functional approach to programming often leads to more reusable, testable, and stable code.
Functional programming can be difficult to understand at first, especially due to its complex terminology. Rather than learning more theoretical aspects of functional programming, we’ll look at some examples of how to improve existing JavaScript code using functional programming (though these concepts can be applied to most languages).
Iterating with forEach
for
loops are commonly used to iterate over collections in many languages including JavaScript. Let’s say we have an array of values we need to display in alerts individually.
const beatles = ['John', 'Paul', 'George', 'Ringo']
for (const beatle of beatles) {
alert(beatle)
}
This example can be simplified further with a newer JavaScript function called forEach
.
const beatles = ['John', 'Paul', 'George', 'Ringo']
beatles.forEach(beatle => alert(beatle))
Using forEach
we can pass a function called an iteratee, which is called with each value of the array, giving us the same result. forEach
is called a high order function because it takes a function as an argument. This is a powerful functional programming technique for composing functionality with small, simple functions. Because alert
is a function and functions are also values in JavaScript, we can refer to the existing alert
function instead of wrapping it in a new function.
const beatles = ['John', 'Paul', 'George', 'Ringo']
beatles.forEach(alert)
Creating new arrays with map
One of the most common use cases for iterating over an array is modifying each item of the array in place. For example, let’s take an array of numbers and round them individually. We will also need to keep track of the current array index.
const numbers = [1.4, 2.6, 3.14]
let index = 0
for (const number of numbers) {
numbers[index] = Math.round(number)
index++
}
numbers // [1, 3, 3]
numbers
is in the preferred format, but what if we’re only formatting numbers to make them easier to read, and we still need the decimal places for precise calculations? We’ve lost precision of our numbers, and any math code relying on the same array may have less accurate results. This is an unwanted side effect of rounding numbers
in a for
loop. Instead, we can take a more functional approach and create a new array instead of modifying the values in the original array, preventing subtle bugs and promoting reuse.
const numbers = [1.4, 2.6, 3.14]
const rounded = []
for (const number of numbers) {
rounded.push(Math.round(number))
}
rounded // [1, 3, 3]
This is safer, but the nature of for
loops still requires the side effect of pushing to the new rounded array. What if we could iterate over the array like with forEach
, but create a new array by applying Math.round
to every value? Fortunately, there’s a similar function called map
which returns a new array based on the return values of the given function.
const numbers = [1.4, 2.6, 3.14]
const rounded = numbers.map(Math.round) // [1, 3, 3]
This gives us the result we want without modifying a temporary array variable. We could even replace Math.round
with a different pure function, and the original numbers
would remain the same. These functional changes have added a lot of safety and reusability to our code, without requiring us to write in a different functional programming language.
Creating new values with reduce
The map
function is handy for updating array values individually, but we often need to calculate a value that isn’t an array of the same length as the original. For example, let’s add numbers in an array together.
let sum = 0
const numbers = [1, 2, 3, 4]
numbers.forEach(number => sum += number)
sum // 10
We need to modify our sum
here to add each number
to it, but there’s a more pure way to combine these values without creating side effects in a temporary sum
variable. Like the functions we’ve tried so far, reduce
iterates over each value of an array, except it lets us build any new value (not just an array) one step at a time. The reduce function works by passing a special value called an accumulator along with each value of the original array, which is replaced by the return value of our given function after iterating over each value.
const numbers = [1, 2, 3, 4]
const sum = numbers.reduce((accumulator, number) => accumulator + number) // 10
We can combine our numbers into a new accumulator
value individually, without modifying the original array of numbers. Here our iteratee function is called a reducer because it takes two values (the previous accumulator
and the current number
) and reduces (combines) them into a single value, our new sum
. The initial value of accumulator
defaults to the first array value (1 in this case), so reduce
will add each number in sequence to accumulator
until we reach our final result. This initial value makes sense for a sum, but sometimes we need to change the initial value too. Let’s build a new array by doubling each of its numbers.
const numbers = [1, 2, 3, 4]
const doubled = numbers.reduce((accumulator, number) => accumulator.concat(number * 2), []) // [2, 4, 6, 8]
Along with our reducer function, we also pass add a second argument which overrides the initial accumulator
to an empty array (instead of the first item, 1). This is necessary because you can’t call concat
on a number, so we need to wrap the first number in an array like all the others. This end result is similar to map
, which we just reimplemented ourselves by concatenating each number
one at a time. map
is an example of a function that can be replaced with reduce
, but functional code is often easier to read with map
.
const numbers = [1, 2, 3, 4]
const doubled = numbers.map(number => number * 2) // [2, 4, 6, 8]
Filtering arrays with filter
Let’s write a reducer that concatenates only even numbers into a new array.
const numbers = [1, 2, 3, 4]
const isEven = number => number % 2 === 0
const even = numbers.reduce((accumulator, number) => isEven(number) ? accumulator.concat(number) : accumulator, []) // [2, 4]
As powerful as reduce
is, there are other simplified high order functions besides map
. The filter
function also iterates over array values, but instead of returning a new accumulator
for each value, it only concatenates values into the new array if a certain condition is truthy. Let’s use filter
to get even numbers from our array using our isEven
function. isEven
is an example of a predicate, a function that takes a value and returns a boolean.
const numbers = [1, 2, 3, 4]
const isEven = number => number % 2 === 0
const even = numbers.filter(isEven) // [2, 4]
Testing conditions with some and every
Sometimes we want to use predicates to test the values in existing arrays, without creating new arrays. some
and every
also take predicates, but they return true if our predicate is true for some (any) or every (all) values of the array respectively.
const numbers = [1, 2, 3, 4]
const isEven = number => number % 2 === 0
const someEven = numbers.some(isEven) // true
const everyEven = numbers.every(isEven) // false
We could alternatively use a for
loop with new variables or even reduce
, but these simpler functions clarify the intent of our code while avoiding the side effects of a for
loop.
Function composition
Sometimes a more complex reducer can be simplified by calling multiple pure high order functions together. Let’s combine some of the concepts we reviewed and square all the even numbers in our array.
const numbers = [1, 2, 3, 4]
const isEven = number => number % 2 === 0
const square = number => Math.pow(number, 2)
const evenSquared = numbers.reduce((accumulator, number) => isEven(number) ? accumulator.concat(square(number)) : accumulator, []) // [4, 16]
Our reducer is combining two concepts: filtering if number
is even and mapping number
to its square. This sounds a lot like what filter
and map
already do, but because they receive and return new arrays, we can use them together. Combining smaller functions together to build bigger functions is called function composition.
const numbers = [1, 2, 3, 4]
const isEven = number => number % 2 === 0
const square = number => Math.pow(number, 2)
const evenSquared = numbers.filter(isEven).map(square) // [4, 16]
Our newer functional code is easier to read, while still taking advantage of our isEven
predicate and our square
iteratee. Like any effective software design practice, functional programming lets us separate functionality into simpler units of code (like our functions) that are easier to reuse and test in isolation.