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’s reduce function, but — by convention — compose runs right-to-left, so we’ll use reduceRight.
  • 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 our fns array.
  • Finally, reduceRight has an optional second argument, which defines its initial value. In this case, it should be x.

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, and tap. 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 use compose / pipe.

If you have any questions or you’d like to read a piece that digs deeper into particular FP concepts, let me know!

© 2024 Bret Cameron