Understanding the pure function

What defines a pure function? More importantly, when should you use them in a project?

By Luke Whitehouse on

JavaScript

Article

Recent years have seen the rise of Functional Programming in the Web Industry and developers everywhere are being exposed to the different pillars that make up this way of structuring an application.

One such pillar is the concept of a "pure function", and I'm not ashamed to say that it used to confuse the hell out of me. Whenever I read up on the subject I was left with more questions than answers, with most being written from the perspective of a computer scientist, rather than the budding developer.

Needless to say, I'm no expert on computer science but from my experiences as a Front End Developer at Sky UK, I’d like to believe I can shed a little light on the subject and help those of us without a PhD in Astrophysics.

Defining a pure function

As the Pure-bloods in Harry Potter, a pure function doesn't like to mingle with anything not of its own kind.

The Malfoy's don't mingle with mudbloods. (Any excuse to get a Harry Potter reference in to be honest)

Put another way, it doesn't concern itself with anything outside of its own scope. Going from the truth of all information, Wikipedia, you might describe a function as being pure if the following statements ring true:

"The function always evaluates the same result value given the same argument value(s). The function result value cannot depend on any hidden information or state that may change while program execution proceeds or between different executions of the program, nor can it depend on any external input from I/O devices (usually—see below)."

"Evaluation of the result does not cause any semantically observable side effect or output, such as mutation of mutable objects or output to I/O devices (usually—see below)."

https://en.wikipedia.org/wiki/Pure_function

Bit of a mouth full right? Yeah, that's what I thought... but taking out the fluff we can simplify it into a few easier to digest rules:

  1. The function must not rely on any code outside of its scope, or hidden from view.
  2. The function must not modify any code outside of its scope.
  3. The function must always evaluate to the same result, given the arguments are the same.

Still a bit wordy? No worries, we can learn by example.

Note: Functional Programming is a paradigm that can be found in any language. Whilst examples throughout this article will be written in JavaScript, the concepts present are transferable to any programming language that supports the notion of Functional Programming.

Example 1: Greeting

Writing a pure function can be as simple as returning data based on an argument. An example of that could be a greeting.

function greeting(name) {
  return `Hello, ${name}`;
}

greeting("Luke");

Going through our checklist, we know that this function doesn’t rely on any code outside of its scope, nor does it modify anything, so we know that no matter where this function is positioned in a code base the result will always be the same, given that the argument of name is also the same. In this case, providing name with the value of "Luke" will always result in the returned string of "Hello, Luke". Nothing outside of the function can affect it or vice versa.

That's it. Pure functions, tick ✅...

... just kidding, let's take a look at a few more examples.

Imagine we've got an application with login access and we want to check whether or not the user is authenticated. We could create a function that checks this through a conditional statement, which then returns with a boolean. This is known as a Predicate Function, a pretty common practice in applications and something I'm sure we've all used even if you didn't know it had a name.

Here's what you might be tempted to write.

function isUserAuthenticated() {
  return user.authenticated ? true : false;
}

and afterwards, we can call isUserAuthenticated(). However, doing so would violate rule 1.

❌ The function must not rely on any code outside of its scope, or hidden from view.

Why? Well, imagine the following scenario.

let user;
console.log(isUserAuthenticated()) // false
user.authenticated = true;
console.log(isUserAuthenticated()) // true

By modifying the user outside of our function we are also modifying the function's output. Relying on code outside of its scope is impure.

So, how would we turn that into a pure function?

function isUserAuthenticated(user) {
  return user.authenticated ? true : false;
}

By passing through the user as an argument of the isUserAuthenticated() we are ensuring that the check is only done to code within the scope of the function.

✅ The function must not rely on any code outside of its scope, or hidden from view.

Now when we look at the output from various manipulations of a user variable outside of its scope, it will not affect the end result.

let user;
var bob = { authenticated: true };
console.log(isUserAuthenticated(bob)) // true
user.authenticated = true;
console.log(isUserAuthenticated(bob)) // true
user.authenticated = false;
console.log(isUserAuthenticated(bob)) // true

Predicate functions use the concept of a pure function to great effect, but let's take a look at some more meaty examples. Adding to the basket should suffice.

Example 2: Adding an item to the basket

Let's use our crazy imagination once again to pretend we have an eCommerce store. Now aside from supplying the best source of "GoodVibes" around, it also has a basket where a user's list of items they'd like to purchase are stored. If we wanted to update that basket with a new item or delete an old one, we might create a function to do that.

let basket;
function addToBasket(item) {
basket.items.push(item);
}

addToBasket('Excellent Vibes');

Unfortunately, this would result in the violation of rule 2.

❌ The function must not modify any code outside of its scope.

By pushing item to the array of items within basket you are modifying the state of basket. This can cause side effects within your code and is generally something to avoid. Imagine you have another function that's also doing something to your basket, it can be pretty painful to debug if an error is discovered and can lead to many regressions.

Instead, to make this pure we can pass through the basket as an argument, similar to how we tackled the predicate function.

function addToBasket(basket, item) {
  return { ...basket, items: [ ...basket.items, item ] };
}

By assigning the basket argument to a new variable we are essentially cloning the basket for use within this function. This allows for us to modify it but not affect the rest of the application. Once we're finished, we can then return the updated version of our basket for our application to utilise.

Heres that in action.

var basket;
var excellentVibesProduct;
addToBasket(basket, excellentVibesProduct);

Example 3: Calculating tax

Moving on from the basket, once a user gets into the checkout of our application we'll need to calculate the price of their basket. In order to do that, we'll first need to calculate the price after VAT has been added.

var vatRate = 0.2;
function calculatePriceAfterTax(price) {
  return (price * vatRate) + price;
}
calculatePriceAfterTax(20.43);

Once again this violates our first rule:

❌ The function must not rely on any code outside of its scope, or hidden from view.

As vatRate will very rarely change you may be tempted to reference this as a constant. That's what we were always taught right? However, allowing a function to be reliant on code outside of its scope can lead to adverse effects, for example, if vatRate were to change without prior knowledge. The benefit of pure functions is that you're explicitly setting the values into the function. You'll know for a certainty that when the arguments remain the same, so does the output. So when you come back to a project after 6 months you won't suddenly see regressive behaviour for the first time. Simply put, the guess work is taken out.

Example 4: Luck

As a final example, let's take a step back slightly from our eCommerce store of Good Vibes to talk luck. More specifically, how we might use luck as an example of how we can break the 3rd rule.

Let's say we want to pick a random user from our website and give them a voucher or something. Whatever the case, we'd need a way to determine this random user.

const users;
function getRandomUser() { 
  return users[Math.floor(Math.random()*users.length)];
}
getRandomUser(); // random item from `users` array.

Spot the problems?

First, we're violating rule 1.

❌ The function must not rely on any code outside of its scope, or hidden from view.

But as previous, that can be easily solved by passing through the users as an argument to our function.

const users;
function getRandomUser(users) { 
  return users[Math.floor(Math.random()*users.length)];
}
getRandomuser(users); // random item from `users` array passed as an argument.

Better, but we're still violating rule 3.

❌ The function must always evaluate to the same result, given the arguments are the same.

This is where pure functions cannot not be used. Math.random() by definition is an impure function as it's output can be different, and so too will our getRandomUser function. However, that's OK. By definition, we want the output to sometimes be different in this case. The logic we're building is impure, so our function will have to be as well.

Keep in mind that concepts like these have their use cases for sure, but that doesn't mean they're the only way of coding from now on. In certain cases, as you've seen, they cannot or should not be used, and that's perfectly fine.

The benefits of pure functions

OK, hopefully, that gives you enough of an idea of how you might use pure functions. To finish, let's summarise their benefits.

For me, one of the major benefits of a pure function is its ability to remove unintended side effects from your application. Heres a few of the key areas you'll benefit from:

  • Testability: When the input and output of a function are always the same, unit tests become a breeze to write. In addition, when you come to test them later down the line you've got a nice interface to debug from with only two points of contention.
  • Maintainability: A function that doesn't affect anything else in your app makes maintaining a project that bit easier.
  • Reusability: In a world of component-based applications with pattern libraries and the like, creating functions that cannot affect anything outside of its scope, or likewise cannot be affected by anything outside of it's scope become very advantageous, especially when we start thinking about utility functions that are intended to be reused across different projects.

All in all, I'd recommend you give pure functions and the concept of Functional Programming a chance. If you're working with React or similar Front-end frameworks you'll save yourself a lot of headaches simply by restructuring how you work with the most basic of computer language features – the function.

Until next time ✅

Follow us on Twitter, Facebook or Github.
© Copyright 2021 Assortment.
Created and maintained by Luke Whitehouse.