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 |
Top comments (16)
There are a few use cases where an implementation of a function could be considered 'not pure' while, from the outside, it is pure.
Yah I agree that Impure Function is also super useful in certain contexts. Sometimes, we use in-place solution to reduce the space complexity instead of creating copies with Pure Function
Nice article!
I think that it is worth pointing out could be that spread syntax only is pure for primary types. For objects and that sort of stuff it is only the reference that's being copied so any mutations would also mutate the orignal object.
.sort() is also impure, sorting the input in-place.
Thanks for the suggestion, I've updated the list!
I'd like a way to flag pure functions like an annotation or similar. Last time I looked this up I didn't find any so I'm throwing this out - anyone doing this, and how?
example, jsdoc style
I would love that too.
Great article! I would like to add that writing a pure function is more about how much control/ confidence you have on that piece of code, rather than following a bunch of rules and assuming it as a pure function. By confidence/ control, I mean how sure are you that if a certain input is any given later in time, that piece of code will give the same output.
That's how I see pure functions.
And in particular, you would need to assign the result of using the rest operator to a new variable get the sorted array?
For example:
const array = [1,2,6,5,4]
let newArray = [...array].sort((a,b) => a-b )
console.log(newArray) // [1, 2, 4, 5, 6]
console.log(array) // [1,2,6,5,4]
Great, I want to be exact in the language I use so thank you for commenting :) I edited “considered pure” to “typically associated with pure functions”.
As for the definition, I found it elsewhere. Before I update it, let me try and see if I’m parsing what you’re saying correctly: you’re saying that while my definition describes some side-effects, it doesn’t describe ALL side-effects, because there are some which are indeed related to computing the final output. Is that right?
Great write up, educational and entertaining! Definitley going to be using the Slytherin reference re function purity now with my teammates lol
Great lunchtime read! Thanks for the well-researched article!
Thanks
So cool man! Thanks. Best article on web.
Ansible refers pure functions as Idempotent Playbooks which are useful for retries
Nice article