Purity

Damn, this feels good

Motivation

So many of our bugs are rooted in IO related, data mutation, side effect bearing code. These creep up all over our code base — from things like accepting user inputs, receiving an unexpected response via an http call, or writing to the file system. Unfortunately, this is a harsh reality that we should grow accustomed to dealing with. Or is it?

What if I told you, that we could minimize the parts of our code which executed the critical / volatile bits of our program? We could enforce (by convention) that the large majority of our code base would be pure, and limit IO related / side effect bearing code to a specific part of our codebase. This would make our debugging process a lot easier, more coherent and easier to reason about.

So, what is this mythical pure function? A pure function has two main characteristics:

1. A pure function is deterministic. This means, that given the same input, the function will always return the same output. To illustrate this as a function in mathematical terms (this will be quick!) it is a well defined function. Every input returns a single output, every single time.

A pure function, in the wild!

A pure function

const add = (x, y) => x + y // A pure function

add is a pure function because it’s output is solely dependent on the arguments it receives. Therefore, given the same values, it will always produce the same output.

How about this one?

const magicLetter = '*' const createMagicPhrase = (phrase) => `${magicLetter}abra${phrase}`

Something about this one is fishy…. The createMagicPhrase function is dependent on a value which is external to its scope. Therefore, it is not pure!

An impure function

const add = (x, y) => x + y // A pure function

add is a pure function because it’s output is solely dependent on the arguments it receives. Therefore, given the same values, it will always produce the same output.

How about this one?

const magicLetter = '*' const createMagicPhrase = (phrase) => `${magicLetter}abra${phrase}`

Something about this one is fishy…. The createMagicPhrase function is dependent on a value which is external to its scope. Therefore, it is not pure!

An impure function

const fetchLoginToken = externalAPI.getUserToken

Is fetchLoginToken a pure function? Does it return the same value every single time? Absolutely not! Sometimes it will work — sometimes the server will be down and we will get a 500 error — and at some point in the future the API may change so that this call is no longer valid! So, because the function is non-deterministic, we can safely say that it is an impure function.

2. A pure function will not cause side effects. A side effect is any change in the system that is observable to the outside world.

const calculateBill = (sumOfCart, tax) => sumOfCart * tax

Is calculateBill pure? Definitely :) It exhibits the two necessary characteristics:

The function depends only on its arguments to produce a result

The function does not cause any side effects

The Mostly Adequate Guide states that side effects include, but are not limited to:

changing the file system

inserting a record into a database

making an http call

mutations

printing to the screen / logging

obtaining user input

querying the DOM

accessing system state

Why should our functions be pure?

Aside from just being awesome

Readability -> Side effects make our code harder to read. Since a non pure function is not deterministic it may return several different values for a given input. We end up writing code that needs to account for the different possibilities. Let’s look at another http based example:

async function getUserToken(id) { const token = await getTokenFromServer(id); return token; }

This snippet can fail in so many different ways. What if the id passed to the getTokenFromServer was invalid? What if the server crashed and returned an error, instead of the expected token? There are a lot of contingencies that need to be planned for, and forgetting one (or several!) of them is very easy.

Additionally, a pure function is easier to read, as it requires no context. It receives all of its needed parameters up front, and does not talk / tamper with the state of the application.

Testability -> Because pure functions are deterministic by nature, writing unit tests for them is a lot simpler. Either your function works or it doesn’t 😁

Parallel Code -> Since pure functions only depend on their input, and will not cause side effects, they are great for scenarios where parallel threads run and use shared memory.

Modularity and Reusability -> Think of pure functions as little units of logic. Because they only depend on the input you feed them, you can easily reuse functions between different parts of your codebase or different projects altogether.

Referential Transparency -> This one sounds so complicated 🙄🙄 When I first read the title I wanted a coffee break! Simply put, referential transparency means that a function call could be replaced by its output value, without changing the overall behavior of our program. This is mostly useful as a framework of thought when creating pure functions.

It’s pure and all…. but does it do anything?

Let’s get pure

let a = 4; let b = 5; let c = 6; const updateTwoVars = (a) => { b++; c = a * b; } updateTwoVars(a); console.log(b,c); // b = 6, c = 24

It’s important to note that although pure functions offer a ton of benefits, it’s not realistic to only have pure functions in our applications. After all, if we did our application would have no side effects, thus not produce any observable effects to the outside world. That would be pretty boring 😥😥😥. Instead we will try to encapsulate all of our side effects to specific parts of our codebase. That way, assuming we have written unit tests for our pure functions and know they are working, if something breaks in our app, it will be a lot easier to track down.Let’s conclude our discussion by converting the following non pure function to pure. This is a contrived example, but demonstrates how we can easily refactor unpure code to pure.

Let’s start by reviewing why this function is unpure. Our function is unpure because it depends on a and b, which are external to its scope. In addition, it is also directly mutating (changing) the values of the variables. The quickest way to refactor this function is

First ensure that all the variables that the function depends on are passed as arguments

Instead of mutating (manipulating) b and c, we can return new values which will reflect the new values.

let a = 4; let b = 5; let c = 6; const updateTwoVars = (a, b, c) => [b++, a * b]; const updateRes = updateTwoVars(a,b,c); b = updateRes[0] c = updateRes[1]

Summary

Stay tuned for the next post, where we discuss functional compositions in JS, why they will make your code more readable, and how you can start utilizing them immediately.

We’ve covered a lot of the benefits of transitioning our code base to include more pure functions. It makes our code easier to reason about, test, and most importantly more predictable. Remember, pure functions are not about completely ridding our code base of side effects. It’s about constraining them to a definitive location and eliminating as much of them as possible. This approach will justify itself many times over, when your programs start growing in size and complexity.

And most importantly! I am creating these tutorials using Mindflow.ai. Mindflow creates smart summaries of your workflow (nearly) automatically. It makes documenting my work a breeze!

If you’re passionate about Front End development, check out the JavaScript Works job-board here!