DEV Community

Nimmo
Nimmo

Posted on

Pure functions, and why I like them.

Pure functions are not new. This isn't a new concept by any means at all, and this certainly isn't the first post anyone has written about them. But the benefits of pure functions are worth re-stating loud and often, because they make your life better. They're self-contained, they reduce cognitive load, increase testability, lead to fewer bugs, and are inherently reusable.

Before reading on, take a moment to consider what the following functions have in common.

const isOverLimit = x => x > limit
const multiply = x => x * config.requiredMultiple
const getItem = index => store[index]
const spaceAvailable = date => schedule[date].attendees < limitPerDay

Predictability

None of the example functions are complicated by any stretch, but one thing these examples have in common is that you can't look at them and know what their return value will be. You can see that isOverLimit will return true or false, and you can infer that the point of that function is to find out if a supplied value is over a limit imposed by your system, but do you know whether it will return true if you call it with isOverLimit(9000)? You'd have to find out what limit was pointing to for this, increasing your cognitive load unnecessarily, and making you look elsewhere in your codebase to understand the thing you were originally looking at; too much of that leads to distraction and frustration in equal measure, in my experience at least.

Consider this alternative:

const isOverLimit = (x, limit = 100) => x > limit

Now you can look at that function and see exactly what it'll return under any circumstance. You can see that isOverLimit(9000) will be true, and isOverLimit(9000, 9001) will be false.

Re-usability

Think again about my original isOverLimit function. Imagine that my Product Owner comes to me one day and says that our company is adding a new "Gold" membership level to our product, with its own special limit of 1000.
In my original code, perhaps I'd have const isOverGoldLimit = x => x > goldLimit, and I'd maintain limit and goldLimit somewhere. I'd just keep writing this same function for every new membership level introduced, right?

But now that my isOverLimit is pure, I can just re-use it:

const isOverGoldLimit = x => isOverLimit(x, 1000)

Testability

So the example multiply function is working nicely in my imaginary system, which due to strict business requirements has to multiply things that we give it by a number that is set through a user's configuration, and can be updated at any time. Thanks to another business requirement, I'm not allowed to know what that number is. And thanks to a third business requirement, I must make sure I have an automated test that proves this function is working correctly. How do I do that? It doesn't take much to realise the answer is either "I can't", or if you're being generous, "with difficulty". But if I re-write it to be a pure function like I did with isOverLimit, it would look like this:

const multiply = (x, y = config.requiredMultiple) => x * y

So, config.requiredMultiple can still be whatever it was before, but crucially I can easily write a test that checks that my function is working: assert.equals(multiply(2, 4), 8)

No side effects

Pure functions can not cause anything to happen to any values outside of the function themselves. Consider the difference between array.push and array.concat in JS:

const updateItemsViewed = item => itemsViewed.push(item)

Great, this allows me to record what items have been viewed. But thanks to the side effect I've introduced here, this function doesn't give me the same output every time it's called with the same input. For example:

let itemsViewed = ['item1', 'item2', item3']
console.log(updateItemsViewed('item4')) // ['item1', 'item2', 'item3', 'item4']
console.log(updateItemsViewed('item4')) // ['item1', 'item2', 'item3', 'item4', 'item4']

Consider again the automated test for this function - the complication you should see immediately is that the test itself will alter my itemsViewed, so when I run it a second time, it will add my test item a second time. You've probably seen this before, where automated tests have a "setup" or "teardown" to deal with "resetting" any side effects the tests themselves have introduced. But if your function was pure in the first place, you wouldn't have this issue:

const itemsViewed = ['item1, 'item2', 'item3']
const updateItemsViewed = (item, itemsViewed = []) => itemsViewed.concat(item)
console.log(updateItemsViewed('item4', itemsViewed)) // ['item1', 'item2', 'item3', 'item4']
console.log(updateItemsViewed('item4', itemsViewed)) // ['item1', 'item2', 'item3', 'item4']

assert.deepEqual(updateItemsViewed('testItem'), ['testItem'])

Obviously the examples in this post are contrived to demonstrate the points I'm making, and of course you can't have a codebase entirely full of pure functions, unless the software you're writing is there to do nothing. But seriously, favour pure functions everywhere you can, and keep all your application's side effects to the "edges", and you'll thank yourself in the future. As will anyone else who has to look at your code. :)

TL;DR

Side effects are best avoided wherever they can be, and if you're strict about using pure functions you'll benefit from a codebase that is much easier to test, much easier to reason about, and much easier to extend and maintain. If your functions can be called without using their return value, then they're either not pure, or they're not doing anything. Either way, you can't re-use them or write tests for them (easily), and I'd strongly suggest you should consider changing them if they're anywhere but the very "edges" of your codebase.

Top comments (3)

Collapse
 
rasmusvhansen profile image
rasmusvhansen

Nice.
A curried version of isOverLimit would be even nicer though. Then you could define
isOverGoldLimit = isOverLimit(1000)

Collapse
 
nimmo profile image
Nimmo

Sure, but the example would be more complicated for anyone who isn't familiar with basic FP concepts, and this was intentionally as basic as possible. :-)

Collapse
 
rasmusvhansen profile image
rasmusvhansen

Good point :-)