Why Do You Need to Know About Functional Programming
When I first started learning about functional programming, I had a hard time wrapping my head around it. I understood the concept and the main principles but I lacked the practical knowledge. With this tutorial, I want to cover not just the concepts, but give you examples and show you how you can apply the functional programming paradigm to your own code.
Letโs first start by defining what is functional programming.
Functional programming is a programming paradigm.
Just like object-oriented programming, functional programming has its own concepts. For example, everything revolves around being pure โ functions always return the same output given the same input. They have no side effects, meaning they donโt alter or mess with any data outside of their scope.
It also advocates being immutable โ once something is created, it cannot be changed. We may also often hear that functional programming uses a declarative approach as opposed to the imperative approach that is also used by the object-oriented paradigm.
These are just some of the concepts that make up functional programming. But why are these principles important? What can they give us?
Why Functional Programming Can Benefit Us
Itโs important to mention that functional programming is not a new paradigm. In fact, Lisp which was developed in the late 1950s was heavily functional. Still, we can benefit from it today for a couple of reasons.
One of them is that it will make your code easier to reason about. It focuses more on the โWhat is your program doing?โ instead of โHow does it do its thing?โ โ meaning you go with a declarative approach opposed to imperative implementations. To demonstrate, take a look at the two examples below.
In the first example, you focus on how the program is doing its thing, while in the second, you focus on what the program is doing:
Imperative
for (let i = 0; i < products.length; i++) {
products[i].price = Math.floor(product.price);
}
Declarative
products.map(product => {
product.price = Math.floor(product.price);
return product;
});
The two implementations are doing the same thing; modifies an array so we have rounded numbers for each product. For this small example, it may seem like you are writing more code. But behind the scenes, map
will also return you a brand new array, meaning your original products
will be kept intact. This is immutability in action.
It also makes your code more easily testable as it focuses on small contained functions called pure functions. As mentioned before, these functions are deterministic. We can guarantee that if we keep passing it the same value, we get the same output.
In the end, functional programming makes your code easier to reason about. It makes it easier to read and follow the process you took and makes your application less prone to bugs. In case something still goes wrong, itโs easier to troubleshoot since your code is more concise.
To demonstrate how you can use functional programming in action, Iโve prepared some code examples that show you how to be declarative.
Declaring What You Mean
One of the best ways to start is by looking at array functions. Higher-order array functions are a good example of the functional programming approach.
Array functions
I have an entire article describing some of the array methods mentioned here, which you can check in the link below:
but letโs quickly go through some of the more important ones and see what they do and how they shorten our code to make it more readable.
Array.prototype.find
Used for finding a specific element that passes the test, returns the first match
// Even if we have multiple products that are on sale, it will only return the first match
products.find(product => product.onSale);
Array.prototype.filter
Used for returning the elements that pass the test, returns every match
// This will return every product that is on sale
products.filter(product => product.onSale);
Array.prototype.every
If every element meets the criteria, it will return true
// Every product should have a name so we get back true
products.every(product => product.name);
Array.prototype.some
If at least one element matches the criteria, it will return true
// If we have at least one product that is on sale, we get back true.
products.some(product => product.onSale);
Array.prototype.map
Used for transforming an array, gives back a new one
// Rounding prices for products
products.map(product => {
product.price = Math.floor(product.price);
return product;
});
Array.prototype.reduce
Used for producing a single value from an array
// Sum the prices of each product
products.reduce((accumulated, product) => accumulated + product.price, 0);
You can already see how these array methods can shorten our code instead of using for loops, but we can make them even more powerful by chaining them. Most of these functions return an array, on which we can call another method and keep going until we get the desired result.
Function chaining
Function chaining is another great concept. It makes your code more reusable and again, reduces the noise and creates a shorter, more concise code that is both more readable, and in case of any bugs, itโs easier to debug.
In the example below, youโll see that since each function call returns an array, you can keep calling new functions on them to create a chain.
const round = (num) => Math.floor(num);
const isDivisibleByTwo = (num) => num % 2 === 0;
const add = (accumulated, num) => accumulated + num;
const numbers = [0, 1.2, 2.4, 3.6, 4.8, 5, 6.2, 7.4, 8.6, 9.8];
const sum = numbers.map(round)
.filter(isDivisibleByTwo)
.reduce(add, 0);
Instead of using three different for loops to get the desired value, we can simply call functions one after another and get it done in 3 lines.
Last but not least, libraries can help you avoid writing down the same things over and over again โ and reinventing the wheel โ by introducing helper functions for commonly occurring problems.
Libraries
There are many libraries out there that are following the functional programming paradigm. Some of the more well known are Lodash and Ramda. To give you some visual differences between the two letโs take a look at how you can retrieve nested properties in each โ a commonly occurring problem. If one of the objects does not exist, we will get an error saying:
Letโs say we have a user object where we want to get their email address:
const user = {
name: 'John Doe',
dob: '1999.01.01',
settings: {
email: 'john@doe.com'
}
}
Lodash
Lodash uses underscore
// returns "john@doe.com" || undefined
_.get(user, 'settings.email');
Ramda
Ramda uses R
// returns "john@doe.com" || undefined
R.path(['settings', 'email'], user);
In each library, we can avoid getting an error if the parent of email
does not exist. Instead it silently fails with an undefined
.
Now you have a better understanding of how to be more declarative. What are some other important concepts in functional programming? โ Itโs in the name, it is functions.
Functions
Functions are not only an essential part of functional programming but of JavaScript as well. They can help you break up your code to smaller, more digestible pieces. It increases readability and makes your code more easily testable by separating your code into smaller sections, often called components.
There are many concepts of how we can use functions to our own advantage. Letโs see some of the more commonly occurring definitions you can find in functional programming.
Pure functions
As discussed previously, pure functions donโt depend on any data other than what is passed into them. They also donโt alter any data other than what they returned.
To give you a practical example for pure functions, think of the Math
object:
// This will return ??? - we don't know
Math.random();
// This will return 10, no matter what.
Math.max(10, 5);
Here, Math.random
is impure since it always returns a different value, even if we were to pass it the same input. Math.max
however is a pure function since it will return the same output given the same input.
We need to note that in case our function doesnโt have a return value, it is not pure.
First-class functions
In JavaScript and other functional languages, functions can also be assigned to variables and you can pass them around, just like they were variables.
const greet = function () {
console.log('๐');
}
// The greet variable is now a function, we can invoke it
greet();
Higher-order functions
A higher-order function is nothing more than a simple function that takes in another function as one of its arguments. Functions that return another function are also called higher-order functions.
A great example for higher-order functions are previously discussed array functions such as filter
or map
.
Function composition
Function composition is all about combining functions to form brand new functions.
For example, Ramda has the compose
function which takes in a list of functions as arguments and returns a function. You can call this with the input for which you want to apply the series of functions.
// Produces 7.283185307179586
R.compose(
R.add(1),
R.multiply(2)
)(Math.PI);
Currying
Currying is a technique where you call a sequence of functions with one argument instead of calling one function with multiple arguments. Each function returns another function. The function at the end of the chain returns the actual expected value.
// Instead of
const add = (a, b, c) => a + b + c;
add(2, 2, 2);
// Currying does
const curry = (a) => {
return (b) => {
return (c) => {
return a + b + c;
}
}
};
curry(2)(2)(2);
Recursion
Recursion happens when a function keeps calling itself until some condition is met. In the example below, we are counting down from 100.
finalCountdown = (number) => {
// If we don't specify an exit criteria, the number will continue into minus until the browser crashes
if (!number) {
return;
}
console.log(`It's the final countdown! - ${number}`);
finalCountdown(number - 1);
}
// Will print out numbers from 100 till 1
finalCountdown(100);
Itโs important to specify an exit condition otherwise you will create an infinite loop that eventually crashes the browser.
Now if you feel like you are starting to become overwhelmed by the amount of information, donโt worry, itโs a good sign that means you are expanding your knowledge. There are only two more important concepts we need to cover. They go hand in hand. They are immutability and side effects.
Immutability
When we talk about immutable variables and objects, we simply mean that once declared, their value canโt be changed. This can reduce the complexity of your code and make your implementation less prone to errors.
To demonstrate immutability through an example, letโs say you have an array where you need to remove the first item. Take a look at the differences below:
const presents = ['๐', '๐ฆ', '๐', '๐', '๐'];
// --- Mutable solution ---
// we get back ๐
// and presents will be equal to ['๐ฆ', '๐', '๐', '๐'];
presents.shift();
// --- Immutable solution ---
// newPresents will be equal to ๐ฆ ๐ ๐ ๐
// and presents will be still equal to ['๐', '๐ฆ', '๐', '๐', '๐'];
const newPresents = presents.slice(1);
In the first example, we modify the original array with the shift
function. If we want to achieve the same but keep the original array intact, we can use slice instead. This way you can avoid having unforeseen bugs in your application where you unintentionally modify data that should be kept in pristine condition.
One downside of immutability is performance. If you create too many copies you will run into memory issues so in case you operate on a large data set, you need to think about performance.
Side Effects
We also need to talk about side effects not because they are part of the functional programming paradigm but because they happen regardless of what programming pattern you take. They are an important part of any program and you need to know when and why they happen.
So what are side effects? โ Side effects can occur when a function is impure, therefore it does not necessarily return the same output given the same input. One commonly occurring example would be a network request. No matter what is the input, you can get back anything from 200 (OK) to 500 (Internal Server Error).
So you canโt avoid having side effects and your goal shouldnโt be to eliminate them entirely, but rather to be deliberate. Deliberate about why and when they happen.
Summary
Functional programming is a great way to organize your code in a better way. There are other programming paradigms out there like object-oriented programming. So what should you use, which is better?
Thereโs really no answer, it depends on your situation and thereโs no one above the other. You can also combine multiple paradigms together so itโs not a โone way or the otherโ.
Thank you for taking the time to read this article, happy coding!
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: