Pure functions are without a doubt the most important technique for writing software that I have learnt thus far in my career. This is because they help us simplify our code and make the effects of our code much more predictable.

These super powers are lame — Pure functions are not

What is so great about simple code?

A major part of writing good software is the art of keeping the complexity of our code to a minimum. Reducing complexity is useful when building applications, but damn near critical when working at scale.

So, why is it so important?

Simple code is easier for people to read and understand. We don’t just write code for computers to process, we write it for humans to read as well.

Simple code is easier to extend, change and delete. Well designed code is code that is easy to change.

Simple code is easier to reason about. This makes it easier to debug and validate.

Simple code is easier to write tests for.

Simple code is predictable.

What is a pure function?

A pure function is a function that, given the same inputs, will always return the same outputs.

Let’s look at an example.

const add = (num1, num2) => {

return num1 + num2;

}; const result = add(1, 5);

// result = 6

As we can see, the function add() will always return the same output, given the same input. Regardless of how and when we call the add() function it will always return 6, given the inputs 1 and 5.

This makes the output of this function incredibly predictable. Predictability is great because it makes it easy for anyone reading our code to understand the data flow of our application. Another huge benefit is that it makes testing our code easy.

const add = (num1, num2) => {

return num1 + num2;

}; test('add() should correctly add numbers', () => {

expect(add(1, 5)).toBe(6);

expect(add(1, 0)).toBe(1);

expect(add(4, 100)).toBe(104);

});

The problem of time and shared state

Let’s look at a function that is not pure.

const timeSinceMoonLanding = () => {

const currentTime = Date.now();

const moonLandingTime = -14182980; return currentTime - moonLandingTime;

};

The above function is not pure for two main reasons:

It relies on the Date object which exists outside of the scope of the timeSinceMoonlanding() function.

object which exists outside of the scope of the function. The output of Date.now() changes every millisecond. This means timeSinceMoonlanding() will not return the same output given the same inputs.

Why should we avoid this?

Testing the function becomes more difficult — we have to mock the Date object.

object. Our function is tightly coupled with the Date object. This means the function will break in any environment that doesn’t have access to the Date object.

object. This means the function will break in any environment that doesn’t have access to the object. It much harder to extend the function — what if we want to calculate the time since moon landing for events other than the current time?

To understand how the function works, we need to understand how the Date object works and how it may change over time.

object works and how it may change over time. To understand how the function works, we also need to understand how other parts of our program may operate and change the Date object.

object. If our function changes or modifies the Date object we need to understand how this may effect other parts of our program.

object we need to understand how this may effect other parts of our program. The time and order our functions are executed matters. This opens up a whole new group or bugs such as race conditions.

Our function is harder to reason about and it can become hard to trace how it effects the state of our program.

The behaviour of our function and the effect the function has on our program is unpredictable.

Let’s refactor our function to make it pure. We can do this easily by passing the current time in as a parameter.

const sinceMoonLanding = currentTime => {

const moonLandingTime = -14182980;

return currentTime - moonLandingTime;

}; const moonlandingToNow = sinceMoonLanding(Date.now());

const moonlandingToYear2000 = sinceMoonLanding(946645200000); test('sinceMoonLanding() should return correct duration', () => {

const date1 = 1567563651281;

const date2 = 946645200000; expect(sinceMoonLanding(date1)).toBe(1567577834261);

expect(sinceMoonLanding(date2)).toBe(946659382980);

});

Notice how making our function pure has also made it easier to extend our function. We can now use it to calculate time gaps between the moon landing and other events like the year 2000.

The power of immutability

Immutable data structures are data structures that cannot be changed(mutated) after they are created. It is a powerful technique that allows us to make changes to our data structure without wiping state history. It also eliminates a whole class of nasty bugs(more on that later).

Let’s look at an example where we need to update and change the global state of our application. This example will show us the pitfalls of not using immutability. In this example we directly mutate our global state.

// our apps global state

const appState = {

items: [],

totalCost: 0,

}; function addItem(item) {

appState.items.push(item);

appState.totalCost = appState.totalCost + item.cost;

}; function removeLastItem() {

const lastItem = appState.items[appState.items .length - 1 ]; appState.totalCost = appState.totalCost - lastItem.cost;

appState.items.pop();

}; addItem({

name: 'keyboard',

cost: 35,

}); addItem({

name: 'laptop',

cost: 1250,

}); addItem({

name: 'mouse',

cost: 15,

}); removeLastItem(); const result = appState;

Each function that modifies our state, addItem() and removeItem() , directly mutates our global state. This introduces a whole class of potential problems.

The history of state updates is not preserved — making it hard to debug.

To understand the effects of our function, we also need to understand how other parts of our program may operate, change or rely on the appState object.

object. The time and order our functions are executed matters. This opens up a whole new class or bugs such as race conditions. What happens if multiple parts of our program start operating on appState at the same time?

at the same time? The behaviour of our function and the effect the functions have on our program is unpredictable.

Instead of mutating state, we can write functions that take the current state as an input, and return a new modified state as output. Pure functions are perfect for this.

// our apps global state

const appState = {

items: [],

totalCost: 0,

}; const addItem = (state, item) => {

return {

items: [...state.items, item],

totalCost: state.totalCost + item.cost,

};

}; const removeLastItem = state => {

const lastItem = state.items[state.items .length - 1 ] return {

items: [...state.items].slice(0, -1),

totalCost: state.totalCost - lastItem.cost,

};

}; const stateUpdate1 = addItem(appState, {

name: 'keyboard',

cost: 35,

}); const stateUpdate2 = addItem(stateUpdate1, {

name: 'laptop',

cost: 1250,

}); const stateUpdate3 = addItem(stateUpdate2, {

name: 'mouse',

cost: 15,

}); const result = removeLastItem(stateUpdate3);

This also gives us opportunities for better composition techniques. Let’s look at an example using a pipe to sequence our pure functions together.

import { pipe } from 'rambda'; // our apps global state

const appState = {

items: [],

totalCost: 0,

}; const addItem = item => state => {

return {

items: [...state.items, item],

totalCost: state.totalCost + item.cost,

};

}; const removeLastItem = state => {

const lastItem = state.items[state.items .length - 1 ] return {

items: [...state.items].slice(0, -1),

totalCost: state.totalCost - lastItem.cost,

};

}; const result = pipe(

addItem({ name: 'keyboard', cost: 35 }),

addItem({ name: 'laptop', cost: 1250 }),

addItem({ name: 'mouse', cost: 15 }),

removeLastItem

)(appState);

In summary

Let’s recap the benefits of pure functions and immutability.

They reduce nasty bugs such as race conditions.

Make it easier to debug and reason about our software.

Becomes much simpler to write tests for our functions.

Our software becomes more predictable.

Our software becomes more extendable and composable.

Thanks for reading!