Functional Programming in JavaScript: Introduction and Practical Examples
From pure functions and combinators to compose and containers
Published on
Oct 15, 2019
Read time
11 min read
Introduction
Functional programming (FP) is a style of coding that’s been growing in popularity. There’s a lot of content out there that explains whatfunctional programming is, but there’s much less about how to apply it. For me, knowing how to apply it is far more valuable: You can only get a real understanding of and feel for a programming style when you put it into practice. So that’s what this piece intends to be — a practical introduction to the functional programming style in JavaScript.
Unlike some pieces I’ve encountered, this one isn’t going to encourage you to use higher-order functions like map
, filter
and reduce
and leave it at that. Yes, these functions are a useful part of a functional programmer’s toolkit, but they’re only part of the overall picture — many codebases use them without adhering to other functional programming principles. Instead, we’ll be using vanilla JavaScript to build functions that help us stick as closely as possible to the functional paradigm. But first, we need to understand two key functional concepts.
Note: ES6 JavaScript features such as arrow functions and the spread operator make writing FP code a lot easier, so it’s recommended that you follow along in an ES6-friendly environment!
Photo by Erda Estremera on Unsplash
Concept 1. Pure Functions
These are at the heart of functional programming. A pure function has three properties:
1. The same arguments must always lead to the same outcome.
// This function is pure: if the input is the same, the result will always be the same.
const cubeRoot = (num) => Math.pow(num, 1 / 3);
// This function is impure: here, the same argument can produce different results.
const randInt = (min, max) => {
return parseInt(Math.random() * (max - min) + min);
};
2. A pure function cannot depend on any variable declared outside its scope.
const stock = ["pen", "pencil", "notepad", "highlighter"];
// This function is impure: it refers to the stock variable in the global namespace._
const isInStock = (item) => {
return stock.indexOf(item) !== -1;
};
// This function is pure: it does not depend on any variables outside its scope.
const isInStock = (item) => {
const stock = ["pen", "pencil", "notepad", "highlighter"];
return stock.indexOf(item) !== -1;
};
// This function is also pure: all variables are passed in as arguments.
const isInStock = (item, array) => {
return array.indexOf(item) !== -1;
};
3. There must be no side effects: That means no changes to external variables, no calls to console.log
, and no triggering of additional processes.
let fruits = ["apple", "orange", "apple", "apple", "pear"];
// This function is pure: it does not change the fruits variable.
const countApples = (fruits) =>
fruits.filter((word) => word === "apple").length;
// This function is impure: it 'destructively' changes the fruits variable (a side-effect).
const countApples = () => {
fruits = fruits.filter((word) => word === "apple");
return fruits.length;
};
These features make pure functions similar to functions in Mathematics. By using pure functions as often as we can, we keep our code much more transparent, predictable, and self-contained. This makes our code easier to maintain and debug. It also encourages us to split large tasks into simpler, more manageable steps.
Concept 2. Combinators
Combinators are similar to pure functions, but they’re even more restricted. A combinator has the same requirements as a pure function, plus one more:
- A combinator contains no free variables.
A free variable is any variable whose values cannot be accessed independently. Every variable in a combinator must be passed through parameters.
So the following function is pure, but it is not a combinator. It depends on a conversionRates
variable, and we cannot access this independently: It is not a parameter of the function.
const convertUSD = (val, code) => {
const conversionRates = {
CNY: 7.07347,
EUR: 0.90625,
GBP: 0.796313,
INR: 71.1427,
USD: 1,
};
if (!conversionRates[code]) {
throw new Error("This currency code is not available");
}
return val * conversionRates[code];
};
To make convertUSD
a combinator, we’d need to pass in the conversion rate data as a parameter.
By contrast, the functions below are combinators:
const add = (x, y) => x + y;
const multiple = (x, y) => x + y;
const sum = (...nums) => nums.reduce((x, y) => x + y);
const product = (...nums) => nums.reduce((x, y) => x * y);
It should be clear that add
and multiply
contain no free variables, but what about sum
and product
? Surely they introduce two new variables x
and y
which aren’t parameters? But in this case, the values of x
and y
are directly determined by the arguments passed to each function. Rather than representing new variables, x
and y
are aliases for existing variables, so we can also define sum
and product
as combinators.
Note: Usually in functional programming, combinators take and return functions. So in practice, you won’t often see functions like the ones above described as combinators, even though they follow all the rules! For a classic FP combinator, see the compose
function, in the section “Introducing the compose Function” below.
Why Do Functional Programmers Shy Away From Loops?
There’s a lot of content on the web telling us that functional programmers avoid using for
or while
loops, but it rarely explains why. So here’s an example. Below is a function used to create an array of values, from 1
to 100
:
const list1to100 = () => {
const arr = [];
for (let i = 0; i 100; i++) {
arr.push(i + 1);
};
return arr;
};
Using a for
loop like this, we are likely to need two free variables ( arr
and i
). This prevents the for
loop from being a combinator. Technically speaking, it is a pure function, because it doesn’t mutate any variables outside of local scope. But we’d prefer to avoid mutation at all — if possible.
Here’s an alternative that would better fit the functional programming paradigm:
const list1to100 = () => {
return new Array(100).fill(undefined).map((x, i) => i + 1);
};
Now we’re no longer defining any new variables (in this case, i
refers to the index of items in the array, a value which became stored in memory when the array was created). We’re not mutating any variables, either. This function has much more of an FP flavour!
Why Are Pure Functions and Combinators Useful?
If you’re new to functional programming, you might think that we are imposing a lot of unnecessary restrictions on ourselves. You might even be wondering if it’s possible to write an entire application in this way and still follow the rules!
The core idea is that functional programming is easier to write and easier to understand. We can use pure functions in any context: They’ll always return the same result, and they won’t change any other part of the code. Combinators are even more transparent: Every variable in a combinator is something you’ve chosen to pass in.
As for writing real-world applications, it’s true that functional programming can only take us so far. We need to debug our applications by logging things to the console. We also need to mutate variables (e.g., to control state), trigger external processes (e.g., CRUD operations on a database), and process unknown data (e.g., user inputs). The FP approach to these requirements is to cordon off non-FP code into containers and provide bridges between this and our neat, reusable FP code. By keeping mutable code in containers, we limit its ability to become confusing and affect things that we don’t want it to affect!
In the remainder of this piece, we’ll look into some practical ways to:
- write FP code
- solve some of the problems identified above
We’ll begin by building a utility to make functional programming more natural: compose
.
Photo by Bill Oxford on Unsplash
Introducing the compose Function
A very common task in functional programming is to combine multiple functions into one. By convention, we call the function that does this compose
. This function also happens to be a combinator!
Let’s imagine we need a function that converts cents to dollars. FP encourages us to split this task into smaller subtasks, so let’s begin by making four functions: divideBy100
, roundTo2dp
, addDollarSign
and addSeparators
.
const divideBy100 = (num) => num / 100;
const roundTo2dp = (num) => num.toFixed(2);
const addDollarSign = (str) => "$" + String(str);
const addSeparators = (str) => {
// add commas before the decimal point
str = str.replace(/(?!\.\d+)\B(?=(\d{3})+\b)/g, `,`);
// add commas after the decimal point
str = str.replace(/(?<=\.(\d{3})+)\B/g, `,`);
return str;
};
The code above is mostly very simple, except for the addSeparators
function, which uses some nifty regex to add commas before every third digit.
Now we have our functions, we need a way to combine them. The traditional way would be using parentheses, like so:
const centsToDollars = addSeparators(addDollarSign(roundTo2dp(divideBy100)));
That’s okay, but as the number of functions we need increases, keeping track of the parentheses could becoming confusing. That’s where compose
comes in. The compose
function will allow us to combine functions like this:
const centsToDollars = compose(
addSeparators,
addDollarSign,
roundTo2dp,
divideBy100
);
Without the fiddly parentheses, this is much clearer. So, how do we build compose
?
Building a compose Function
Using the higher-order reduceRight
function, this can be achieved in one line:
const compose = (...fns) = x => fns.reduceRight((res, fn) => fn(res), x);
So, what’s going on in the above code?
- First, we use the spread operator
...
to pass in an arbitrary amount of functions as parameters. - Next, we want to turn our array of functions (
fns
) into a single output. We could use JavaScript’sreduce
function, but — by convention —compose
runs right-to-left, so we’ll usereduceRight
. reduceRight
takes a callback function as its first argument. In this callback, we’ll pass two parameters: our result (res
), where we keep track of the latest returned result, and a function (fn
), which we’ll use to trigger each function in ourfns
array.- Finally,
reduceRight
has an optional second argument, which defines its initial value. In this case, it should bex
.
Note: If you’d prefer to order your functions from left to right, you can use reduce
instead of reduceRight
. By convention, this left-to-right variation of compose
would be called pipe
or sequence
.
Now, the following code should return a single function:
const centsToDollars = compose(
addSeparators,
addDollarSign,
roundTo2dp,
divideBy100
);
If we run console.log(typeof centsToDollars)
, we should see "function"
.
Now let’s try it out. If we execute console.log(centsToDollars(100000000))
, we should get a result of $1,000,000.00
. Perfect!
We’ve just written our first real-world example of functional code! No longer want to add a dollar sign? Simply remove addDollarSign
as an argument from compose
. Is your initial value in dollars, not cents? Then remove divideBy100
. Because we’re following FP principles, we can be confident that removing these functions won't affect any other part of our code.
We can also easily reuse any of these smaller functions in any part of our codebase. For example, we may want to use addSeparators
to format other numbers in our application. Functional programming means we can reuse that function without worry!
Debugging compose
But we have a new problem. Imagine we’re using our handy addSeparators
function to format 20 different numbers in our application, but something’s not working. Normally, we might add a console.log
statement to the function to see what’s going on:
const addSeparators = (str) => {
str = str.replace(/(?!\.\d+)\B(?=(\d{3})+\b)/g, `,`);
str = str.replace(/(?<=\.(\d{3})+)\B/g, `,`);
console.log(str);
return str;
};
But that’s not much help because the function triggers 20 times every time our application loads, so we’d see 20 instances of console.log
!
We need a way to see what’s going on only when addSeparators
is called as part of centsToDollars
. To do this, we can use a combinator known as tap
.
The tap Function
The tap
function runs a function with a supplied object and then returns that object:
const tap = (f = (x) => {
f(x);
return x;
});
This allows us to run additional functions in between the various functions passed to compose
without affecting the result: That makes tap
an ideal place to log to the console.
The trace Function
We’ll call our logging function trace
, and we’ll call console.log
as the callback function:
const trace = (label) => tap(console.log.bind(console, label + ":"));
Notice that we have to use bind
to ensure that the global console
object is available when tap
is executed. The next parameter, label
, allows us to add a string in front of whatever is logged to the console, which can make debugging a lot clearer.
Back in compose
, we can add trace
functions to keep track of the object as it is being passed from object to object:
const centsToDollars = compose(
trace("addSeparators"),
addSeparators,
trace("addDollarSign"),
addDollarSign,
trace("roundTo2dp"),
roundTo2dp,
trace("divideBy100"),
divideBy100,
trace("argument")
);
Now if we run centsToDollars(100000000)
, in our console we’ll see:
argument: 100000000 divideBy100: 1000000 roundTo2dp: 1000000.00 addDollarSign: $1000000.00 addSeparators: $1,000,000.00
If there was a problem at any stage, it would now be much easier to spot!
To see all the functions we’ve created so far, including the centsToDollars
example, check out this gist.
Photo by sergio souza on Unsplash
Containers
In the final part of this piece, we’ll take a quick look into containers. We can’t completely avoid messy, stateful code, so functional programming’s solution is to cordon this code off from the rest of our codebase. That keeps all the mutable, side-effect-y, impure code in one place, which keeps things clean. Our pure logic can interact with this code using bridges— methods that we create to trigger side effects and mutate variables in a controlled, predictable way.
First, let’s create a couple of utility functions that will help us check that functions are being passed as parameters:
const isFunction = (fn) =>
fn && Object.prototype.toString.call(fn) === "[object Function]";
const isAsync = (fn) =>
fn && Object.prototype.toString.call(fn) === "[object AsyncFunction]";
const isPromise = (p) =>
p && Object.prototype.toString.call(p) === "[object Promise]";
We’ll be using ES6 class syntax to create our container, but you could also use a regular function for the same purpose:
class Container {
constructor(fn) {
this.value = fn;
if (!isFunction(this.value) && !isAsync(this.value)) {
throw new TypeError(
`Container expects a function, not a ${typeof this.value}.`
);
}
}
run() {
return this.value();
}
}
Our constructor
takes a function or async function. If neither is provided, it will throw a TypeError
. Then the run
method executes the function.
We can store impure functions inside a container, and these won’t run unless they are called specifically, as below:
const sayHello = () => "Hello";
const container = new Container(sayHello);
console.log(container.run()); // 'Hello'
Of course, our sayHello
function isn’t actually impure. But it could be!
To make out container more useful, it would be good to execute additional functions on the result of our container’s run
method. To do that, let’s add map
as a method in our Container
class:
map(fn) {
if (!isFunction(fn) && !isAsync(fn)) {
throw new TypeError(`The map method expects a function, not a ${typeof fn}.`);
};
return new Container(
() => isPromise(this.value()) ?
this.value().then(fn) : fn(this.value())
)
}
This takes a new function as its parameter. If the result of the original function (this.value()
) was a promise, it chains the new function using the then
method. Otherwise, it simply executes the function on this.value()
.
Now we can chain functions onto the function used to create the container. In the example below, we add a new function, addName
, to the sequence, and we use our tap
function from earlier to log the result to the console.
const sayHello = () => "Hello";
const addName = (name, str) => str + " " + name;
const container = new Container(sayHello);
const greet = container
.map(addName.bind(this, "Joe Bloggs"))
.map(tap(console.log));
When we execute greet.run()
, we should see Hello Joe Bloggs
in our console.
To see the complete code from this section, check out this gist.
There’s a lot more to containers than this piece can cover. For example, the popular tool Redux is, at its core, a container for state management. But hopefully, this example is enough to show you what a container is and why it might be useful.
Conclusion
I hope this piece has given you some practical ways to introduce functional programming into your JavaScript code. The majority of functional programming is simple: It’s about writing pure functions as often as possible. For those who are interested, you can go a lot deeper:
- If you’re willing to invest in a paid course, I highly recommend Michael Rosata’s course Building Declarative Apps Using Functional JavaScript (available on LinkedInLearning or Udemy). I used Michael’s course when I was first getting started with functional programming, and I borrowed a few ideas from him for my final section on containers.
- Another great — free — way to improve your functional programming is to look into the library RamdaJS. It’s designed specifically for a functional programming style and has implementations of several of the functions we built earlier, namely
compose
,pipe
, andtap
. The official Ramda website has links to some good articles to get you started. Christopher Okhravi’s video is also a great introduction, and it complements this piece by giving lots more examples about how to usecompose
/pipe
.
If you have any questions or you’d like to read a piece that digs deeper into particular FP concepts, let me know!
Related articles
You might also enjoy...
How to Automate Merge Requests with Node.js and Jira
A quick guide to speed up your MR or PR workflow with a simple Node.js script
7 min read
Automate Your Release Notes with AI
How to save time every week using GitLab, OpenAI, and Node.js
11 min read
How to Create a Super Minimal MDX Writing Experience
Learn to create a custom MDX experience so you can focus on writing without worrying about boilerplate or repetition
12 min read