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 on 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. You 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. you can guarantee that if you keep passing it the same value, you 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.
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 your 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 your 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 you can call another method and keep going until you 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, you 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, you 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 In Functional Programming
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 you can use functions to your 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, you modify the original array with the shift function. If you want to achieve the same but keep the original array intact, you 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.
What Are The 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!
Top comments (1)
Sorry man but that code is still imperative.
Except that's not true, because you're modifying the elements of the array, which aren't copied.
An actual declarative implementation would be something like this:
Criteria is the plural, you probably mean the singular criterion (Not criterium, by the way, that's something else entirely)
Method chaining isn't a functional concept, it's an Object-Oriented one. Some functional languages do have a pipe operator, but most have a function composition operator.
The main problem with Javascript in this case isn't the syntax though, but the fact that it's slower because you're looping over the same data again and again and again. This can be mitigated by smart compilers that do stream fusion to combine them into a single loop, but I'm not aware of whether the big JS implementations actually do that.
You're on the right track there, but you're forgetting the other important part:
Math.random
also has a side effect, meaning it not only accesses but also modifies external state. If it didn't do this, it would still return the same number every time until something else modified its external state.Technically, currying refers to the process of taking one normal function
a, b, c, etc. => result
and turning it into a curried functiona => b => c => etc. => result
Technically no, as in theory ES6 should optimize tail calls. Sadly none of the big browsers has implemented this so far, which, together with the lack of stream fusion, limits JavaScripts functional capabilities significantly.
In proper ES6, this expression should just run forever
const f = e => f(1+e); f(1)
, but in reality it will still crash at some point and the browser will throw an error.Which is not a thing in javascript. You can pretend a variable is immutable, but it's not, which keeps the compiler from making certain optimizations. Say you have an array
[1, 2, 3]
and want the first few elements. If arrays were truly immutable, JS would not have to copy any data and could just return a new object pointing to the same memory position as the original array but with a different length.All we can do in JS is pretend, agree to not modify an element and hope everybody else respects this choice, but neither can we be sure of it nor can the compiler.
I did a lot of nitpicking, but overall it's a very nice article and I hope it helps a bunch of people discover the nice things FP brings to the programming world :D