Pure vs Impure Functions
Sandra Spanik
Posted on March 18, 2021
Software engineering is full of jargon. Occasionally, to grasp the true meaning of the seemingly simplest of words, one must waddle through many murky layers of complexity (fancy defining this
, anyone?). Thankfully, other times, outwardly inaccessible words can be demystified pretty easily. In this article, we'll deal with the latter case, breaking down pure vs impure functions.
person thinking about the definition of this
1. Pure Functions 👼
To be considered pure, functions must fulfil the following criteria:
- they must be predictable
- they must have no side effects
➡️ Pure functions must be predictable.
Identical inputs will always return identical outputs, no matter how many times a pure function is called. In other words: we can run a pure function as many times as we like, and given the inputs remain constant, the function will always predictably produce the same output. Kind of like when you're a pizza-loving person with lactose intolerance. No, this time won't be different, so stop ogling that 16-incher your flatmate ordered.
➡️ Pure functions must have no side-effects.
A side-effect is any operation your function performs that is not related to computing the final output, including but not limited to:
- Modifying a global variable
- Modifying an argument
- Making HTTP requests
- DOM manipulation
- Reading/writing files
A pure function must both be predictable and without side-effects. If either of these criteria is not met, we're dealing with an impure function.
An impure function is kind of the opposite of a pure one - it doesn't predictably produce the same result given the same inputs when called multiple times, and may cause side-effects. Let's have a look at some examples.
// PURE FUNCTION 👼
const pureAdd = (num1, num2) => {
return num1 + num2;
};
//always returns same result given same inputs
pureAdd(5, 5);
//10
pureAdd(5, 5);
//10
//IMPURE FUNCTION 😈
let plsMutateMe = 0;
const impureAdd = (num) => {
return (plsMutateMe += num);
};
//returns different result given same inputs
impureAdd(5);
//5
impureAdd(5);
//10
console.log(plsMutateMe)
//10 🥳 I'm now double digit, yay!
In the above example, the impure version of the function both changes a variable outside its scope, and results in different output, despite being called with identical input. This breaks both rules of pure functions and as such, it's pretty clear we're dealing with an impure function here.
But let's have a look at an example of an impure function that is not so easy to tell apart from its pure counterpart.
//IMPURE FUNCTION 😈
const impureAddToArray = (arr1, num) => {
arr1.push(num);
return arr1;
};
impureAddToArray([1, 2, 3], 4);
//[1,2,3,4]
impureAddToArray([1, 2, 3], 4);
//[1,2,3,4]
Given the same inputs, the function above will always return the same output. But it also has the side effect of modifying memory in-place by pushing a value into the original input array and is therefore still considered impure. Adding a value to an array via a pure function instead can be achieved using the spread operator, which makes a copy of the original array without mutating it.
//IMPURE FUNCTION 😈
const impureAddToArray = (arr1, num) => {
//altering arr1 in-place by pushing 🏋️
arr1.push(num);
return arr1;
};
// PURE FUNCTION 👼
const pureAddToArray = (arr1, num) => {
return [...arr1, num];
};
Let's look at how we'd add to an object instead.
// IMPURE FUNCTION 😈
const impureAddToObj = (obj, key, val) => {
obj[key] = val;
return obj;
};
Because we're modifying the object in-place, the above approach is considered impure. Below is its pure counterpart, utilising the spread operator again.
// PURE FUNCTION 👼
const pureAddToObj = (obj, key, val) => {
return { ...obj, [key]: val };
}
Why should I care?
If the differences in the above examples seem negligible, it's because in many contexts, they are. But in a large-scale application, teams might choose pure over impure functions for the following reasons:
- Pure functions are easy to test, given how predictable they are
- Pure functions and their consequences are easier to think about in the context of a large app, because they don't alter any state elsewhere in the program. Reasoning about impure functions and potential side-effects is a greater cognitive load.
- Pure functions can be memoized. This means that their output, given certain inputs, can be cached when the function first runs so that it doesn't have to run again - this can optimise performance.
- The team lead is a Slytherin obsessed with the purity status of both blood and functions (are we too old for HP references? I think not).
Pure functions are also the foundation of functional programming, which is a code-writing paradigm entire books have been written about. Moreover, some popular libraries require you to use pure functions by default, for example React and Redux.
Pure vs Impure JavaScript Methods
Certain JS functions from the standard library are inherently impure.
Math.random()
Date.now()
arr.splice()
arr.push()
arr.sort()
Conversely, the below JS methods are typically associated with pure functions.
-
arr.map()
arr.filter()
-
arr.reduce()
-
arr.each()
-
arr.every()
arr.concat()
arr.slice()
Math.floor()
str.toLowerCase()
- the spread syntax
...
is also commonly used to create copies
1. Comparison
So who comes out as a winner in this battle between good and evil? Actually, nobody. They simply have different use cases, for example, neither AJAX calls, nor standard DOM manipulation can be performed via pure functions. And impure functions aren't intrinsically bad, they just might potentially lead to some confusion in the form of spaghetti code in larger applications.
Sidenote: I resent the widely held sentiment that the word spaghetti should ever be associated with anything negative. Get in my tummy and out of coding lingo, beloved pasta. 🍝
I'll leave you with a quick tl;dr comparison table.
👼 Pure Functions 👼 | 😈 Impure Functions 😈 |
---|---|
no side-effects | may have side-effects |
returns same result if same args passed in no matter how many times it runs | may return different result if same args passed in on multiple runs |
always returns something | may take effect without returning anything |
is easily testable | might be harder to test due to side-effects |
is super useful in certain contexts | is also super useful in certain contexts |
Posted on March 18, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.