Functional programming is a style of programming where you avoid mutating (or changing) data directly, but instead create new data. The key to functional programming (and the reasons it's called "functional" programming) are functions, which when given specific input always produce the same output. To put it simply, the goal of functional programming is really to take something complex, and break it down into easily readable and easily testable code.
In this article we're going to look at the following concepts which will hopefully give you a foundational understanding of functional programming, and empower you to start using these in your code:
Note: even though the following code examples are in JavaScript, these same concepts will apply to functional programming in any language.
You may have heard the term "pure functions" before, but what are they exactly? A pure function is a function which takes 1 or more arguments, returns a value, does not mutate (or change) any data, and also is not affected by anything outside of the function.
Here's a simple example of a pure function:
const multiplyBy10 = number => number * 10;
It takes a single number
argument, and returns that value multiplied by 10. So, if we call this function and pass in a value of 5
, it will always return 50
. It doesn't matter if a radio button is checked, the user's permissions, or if it's Saturday, this function will always return the same result when you feed it the same value.
You'll probably run into some resistance as you start working towards functional programming. For example, say you have an array of cars, and you want to remove the last one. Normally, your first through might be something like this:
let cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];
cars.pop();
console.log(cars); // [ 'BMW', 'Toyota', 'Lexus' ];
The problem here is that the cars
variable has been modified directly, breaking the rule of "no side-effects". Instead of thinking "I need to remove an element from this array" try thinking "I need to create a new array which doesn't contain the element". Let's look at a functional approach:
const cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];
const filteredCars = cars.splice(-1, 1);
console.log(cars); // [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];
console.log(filteredCars); // [ 'BMW', 'Toyota', 'Lexus' ];
The difference here is that the original cars
variable is untouched, and instead we have a new variable filteredCars
which contains the value we need. This might seem trite, but consider this function:
// a function which removes the last element,
// ...and returns the updated array
const removeLastItem = items => {
items.pop();
return cars;
}
// define our array of cars
const cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];
// call our function
const filteredCars = removeLastItem(cars);
// check our arrays
console.log(filteredCars); // [ 'BMW', 'Toyota', 'Lexus' ];
console.log(cars); // [ 'BMW', 'Toyota', 'Lexus' ]; // huh?
This function seems straight forward enough. It takes our array of cars, removes the last element using pop()
, and we return our new array. Why does our cars
array end up modified then? The problem is that because arrays are passed by reference instead of value, so the function is actually modifying our original array when we call pop()
, not just a copy of the value.
Now, watch what happens if we take what we learned previously and apply it here:
// a function which removes the last element,
// ...and returns the updated array
const removeLastItem = items => {
return items.splice(-1, 1); // remember, this doesn't modify the array
}
// define our array of cars
const cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];
// call our function
const filteredCars = removeLastItem(cars);
// check our arrays
console.log(filteredCars); // [ 'BMW', 'Toyota', 'Lexus' ];
console.log(cars); // [ 'BMW', 'Toyota', 'Lexus', 'Honda' ]; // much better!
Since the splice()
method returns a new array, instead of modifying the original, our removeLastCar
function is now "pure". This makes the code much easier to test and to read. When you look at the code above, you don't need to examine what removeLastCar
is doing internally or wonder if it is affecting some other part of the application. Since it is pure, you only need to know that it is returning a new value, and you can proceed to see how that new value is used.
Getting in the habit of using const
instead of let
can help force you to think in a functional way.
In order to have success with the functional programming approach, I recommend breaking down complex functions into a series of simpler ones. This not only produces more code reuse, but the code is much easier to read. Let's look at this code:
const isUsernameValid = username => {
// sanitize user input
// validate format
// check if username is available
// return boolean result
}
if (isUsernameValid("my_cool_username")) {
// ...
}
Since isUsernameValid
is packed with so much logic (even in this simplified example) this increases the chances of an unintended side-effect and ultimately makes this function harder to unit test and debug. Let's break this logic down into separate functions:
const sanitizeInput = value => {
// return sanitized value
}
const isValidFormat = value => {
// return true if format is valid
}
const isUsernameAvailable = value => {
// return true if username is available
}
const cleanUsername = sanitizeInput("my_cool_username");
if (isValidFormat(cleanUsername) && isUsernameAvailable(cleanUsername)) {
// ...
}
Now the logic has been broken into smaller, and more reusable functions. Additionally, because each function essentially has one main job, unit testing and debugging these are a snap.
Two key concepts in functional programming are "map" and "reduce". As abstract concepts they can be tricky to grasp, but I find that using the following analogy makes it much simpler to understand:
Using the image above as reference, imagine you're making a salad.
.map(slice)
to apply a slice
function (imagine this will slice the provided veggie) to each veggie in the array. The lettuce is sliced, the tomato, and then the onion..reduce(mix)
to apply a mix
function (imagine this function takes a veggie and mixes it with any previous veggie accumulated). The lettuce is fist, then the tomato slices are mixed in, and finally the onions. reduce
returns a single, new value.In traditional JavaScript you might have some code like this:
let numbers = [1, 2, 3, 4, 5, 6];
for (var i = 0; i < numbers.length; i++) {
numbers[i] += 1;
}
When you look at this example, you first have to take some time to understand what it is doing. At the top you can see it defines an array of numbers. Then, when you examine the parts of the for
loop definition to determine that it is iterating through each item in our numbers
array. Finally, inside the for
loop, by examining the code you'll find that it's increasing each value by 1. Now, granted this isn't a complicated example, but it still requires you to mentally evaluate the code in order to understand it's intent.
Now, compare that to this code:
const numbers = [1, 2, 3, 4, 5, 6];
const addOne = number => number + 1;
numbers.map(addOne);
First, we have the same numbers
array declaration at the top, nothing new there. Next, you'll see we've taken the logic which was in our for loop (increasing each value by 1) and moved it into a new function with a very clear name "addOne". Finally, by using .map()
we iterate over each element in the array.
Notice that instead of the for
loop with all of it's boilerplate and the math operation, we've replaced it with a line of code that reads much more clearly:
numbers
: start with our numbers array.map()
: iterate though each item of the arrayaddOne
: add one to each itemBy using a functional approach, with clearly named functions, you end up with self-documenting code; even without comments, the code basically explains itself.
const
)By using .map()
in our example above, we end up returning a brand new array of the results, instead of modifying (or mutating) the original array. The benefit of this is that you won't have to dig through code to see if numbers
was modified. Imagine you have code like this:
let numbers = [1, 2, 3, 4, 5, 6];
validateNumbers(numbers); // does this modify numbers?
filterNumbers(numbers); // ...or does this one?
doSomethingElse(numbers); // ...maybe this one?
console.log(numbers); // what's the value of numbers?
The problem here is that without digging into the function definitions, we can't tell if the value of numbers
has been modified. Additionally, even if the value is the same, there's no guarantee that will be the case under different conditions. Look at an even simpler example:
let numbers = [1, 2, 3, 4, 5, 6];
doSomethingUnrelated();
doSomethingElseUnrelated();
numbers = getOddNumbers(numbers);
doSomethingUnrelatedAgain();
console.log(numbers); // what's the value of numbers?
Notice that you still need to scan through the code to find that one line which changes the value.
Compare that to this:
const numbers = [1, 2, 3, 4, 5, 6];
doSomethingUnrelated();
doSomethingElseUnrelated();
const oddNumbers = getOddNumbers(numbers);
doSomethingUnrelatedAgain();
console.log(numbers); // we know that numbers hasn't changed
console.log(oddNumbers); // ...and this is our modified array
Even with the noise of the unrelated code, we still have confidence that numbers
hasn't changed and filteredNumbers
contains our modified values. Using const
allows you to work with variables with confidence (in most cases) of their values, without having to mentally parse through the code, or add unnecessary comments.
If you're not doing unit testing yet, you really should be, especially for code which is shared throughout your application (e.g. helper functions). Plus, when you work with functional programming, unit testing becomes as easy as it gets. Let's see a quick example with our removeLastItem
function:
const removeLastItem = items => {
return items.splice(-1, 1); // remember, this doesn't modify the array
};
// some simple test cases
removeLastItem([ 1, 2, 3, 4, 5 ]) == [ 1, 2, 3, 4 ]; // true
removeLastItem([ 1, 2, 3 ]) == [ 1, 2 ]; // true
Since the function doesn't have any side-effects (i.e. getting/setting values outside of the function itself) we can avoid complicated test scenario setups, or using spies to catch function calls. We know that with a given input, this function will always produce the same output.
By this point my hope is that you have a better understanding of functional programming and are ready to start applying in your applications. As you look at your existing code, or while putting together the structure for something new, remember these foundational concepts:
Now go forth and write clearer, more testable code.