How to FP

Pure functionsMore FP concepts

Home»Top-down»Pure functions

A pure function is a function which:

  1. Given the same inputs, always returns the same output, and
  2. Has no side-effects (we'll explain below)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Impure functions (AVOID THESE!)
// This function yields a different result when called multiple times,
// hence it's not a pure function
const getRandomNumber = () => Math.random().toFixed(2)

console.log(`First call impure: ${getRandomNumber()}`)
console.log(`Second call impure: ${getRandomNumber()}`)

// Pure functions
// Calling this function with the same arguments yields the same result,
// even when called multiple times
const add = (x, y) => x + y

console.log(`First call pure: ${add(2, 3)}`)
console.log(`Second call pure: ${add(2, 3)}`)

If you run the above Runkit multiple times, you'll see that the getRandomNumber function returns an unpredictable number between 0 and 1, wheras given the same x and y values, the add function will always return the same result, e.g., add(2, 3) // returns 5.

Because pure functions are referentially transparent, we can use substitution to replace a pure function call with its result without changing the meaning of the program. This makes it easier to reason about our programs.

Returning the same result when called more than once is also called idempotency. And idempotency is a Very Good Thing.

Let's look at another example using shared state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// The pure version
const addPure = (x, y) => x + y

console.log(`First call to  \`addPure(2, 3)\`: ${addPure(2, 3)}`)
console.log(`Second call to  \`addPure(2, 3)\`: ${addPure(2, 3)}`)

let x = 2 // Shared state: DON'T DO THIS!

// Our impure version depends on a variable outside its scope
const addImpure = y => x + y

console.log(`\`x\` is \`2\`. Let's call \`addImpure(3)\`: ${addImpure(3)}`)

x = 6

console.log(`\`x\` is now \`6\`. Now let's call \`addImpure(3)\` again: ${addImpure(3)}`)
console.log('Oops.')

Avoid side effects

A side effect is any application state change that is observable outside the called function other than its return value. Side effects include:

  • Modifying shared state: any external variable or object property (e.g., a global variable, or a variable in the parent function scope chain)
  • Logging to the console
  • Writing to the screen
  • Writing to a file
  • Writing to the network
  • Triggering any external process
  • Calling any other functions with side-effects
1
2
3
4
5
6
// This function depends on a side effect (AVOID THIS)
const displayName = name => console.log(`My name is ${name}`)

const result = displayName('Prateek') // Logs 'My name is Prateek' to the console

console.log(`displayName('Prateek') returned ${result}`)

As you can see, the effect of calling displayName was to log a message to the console. The result returned from the function was undefined. So the effect of the function was not solely dependent on the value returned from the function. This, too, is an impure function and should be avoided whenever possible.

Avoiding side effects makes it much easier to understand how a program works, and much easier to test it as well.

Avoid shared state

Shared state is any variable, object, or memory space that exists in a shared scope, or as the property of an object being passed between scopes. A shared scope can include global scope or closure scopes. Often, in object-oriented programming, objects are shared between scopes by adding properties to other objects.

For example, a computer game might have a master game object, with characters and game items stored as properties owned by that object. Functional programming avoids shared stateinstead relying on immutable data structures and pure calculations to derive new data from existing data.

The problem with shared state is that in order to understand the effects of a function, you have to know the entire history of every shared variable that the function uses or affects. Race conditions are a very common bug associated with shared state.

(Note: although JavaScript is single-threaded, it is still possible to share state and to create problems that way.)

Another common problem associated with shared state is that changing the order in which functions are called can cause a cascade of failures because functions that act on shared state are timing dependent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// With shared state, the order in which function calls are made
// changes the result of the function calls
const x = { value: 2 }

// These operate on the shared state x
const incrementX = () => x.value += 1
const doubleX = () => x.value *= 2

incrementX()
doubleX()

console.log(`The value of x after increment then double is ${x.value}`)

// Reset x
x.value = 2

// Run the functions in reverse order
doubleX()
incrementX()

console.log(`The value of x after double then increment is ${x.value}`)

The above is obviously oversimplified, but it's not difficult to imagine a more complex example where shared state could get you in trouble. Also, shared state makes it difficult to thread or parallel process. In FP, we avoid shared state as much as possible.

Avoid mutating state

Immutability is a central concept of functional programming because without it, the data flow in your program is lossy. State history is abandoned, and strange bugs can creep into your software.

Mutation causes defects. If you have a dollar, and I give you another dollar, it does not change the fact that a moment ago you only had one dollar, and now you have two. Again, mutation erases history, which can manifest as bugs in the program.

Here's an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let mutableDollars = 1 // Mutable binding (don't be doing this)

// Increment mutableDollars
mutableDollars += 1

console.log(`mutableDollars is ${mutableDollars}, but what was it a moment ago?`)
console.log('History has been lost')

const dollars = 1 // Cannot be reassigned (hence, immutable here)

// We can only make a new copy
const newDollars = dollars + 1

console.log(`dollars is still ${dollars}; newDollars is ${newDollars}`)
console.log('History is preserved')

try {
  dollars += 1 // Error!
} catch (e) {
  console.log(`Can't reassign dollars: ${e}`)
}

If you have ever dubgged code where you have tried to figure out where a variable's value changed to undefined, then you'll appreciate immutability.

This means when we practice functional JavaScript, everything is a const. We will never use var or let.

When you need to change the value of a variable, you'll create a new variable. Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const state1 = { saved: false }
const state2 = { saved: false }

// MUTATION! DON'T DO THIS!
function saveByMutation (obj) {
  obj.saved = true

  return obj
}

// IMMUTABLE - returns a copy. DO THIS!
const saveByCopy = obj => ({ ...obj, saved: true })

console.log('saveByMutation(state1) returns:', saveByMutation(state1))
console.log('Now state1 is:', state1)
console.log('saveByCopy(state2) returns:', saveByCopy(state2))
console.log('But state2 is still:', state2)

As you can see from the example above, making a copy of state rather than mutating it means that our initial state remains unchanged, so we have lost no history (we can always discard the previous state if we don't need it).

const does not mean immutable

In JavaScript, it's important not to confuse const with immutability. const creates a variable name binding which can't be reassigned after creation. const does not create immutable objects!

If your variable name is bound to a primitive value such as a number, string, or boolean, then const is "effectively" immutable. But if your variable points to a complex object, then although the object itself cannot be replaced, properties on the object can be mutated. This is not true immutability.

Truly immutable objects can't be changed at all. You can make a value truly immutable by deep freezing the object. JavaScript has a method that freezes an object one-level deep:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const obj = { x: 1, inner: { y: 2, z: 3 } }

// We can update properties of obj even though it is a const
obj.x = 5
console.log(`After obj.x = 5, obj is:`, obj)

// Object.freeze mutates the object, unfortunately, and
// freezes only one level deep
Object.freeze(obj)

obj.x = 9

// Unfortunately, the above fails silently
console.log(`After freezing and obj.x = 9, obj is:`, obj)

// But what happens when I try to set obj.inner.z?
obj.inner.z = 7

// Unfortunately, the above fails silently
console.log(`Despite freezing and after obj.inner.z = 7, obj is:`, obj)

As you can see, the top level primitive properties of a frozen object can’t change, but any property which is also an object (including arrays, etc…) can still be mutatedso even frozen objects are not immutable unless you walk the whole object tree and freeze every object property.

A better way to freeze objects to ensure that they are immutable is to write your own deepFreeze function that returns a completely frozen copy of the object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Object.freeze stupidly mutates in place instead of returning a copy. Doh!
function freezeRecursively (obj) {
  Object.freeze(obj)
  
  Object.getOwnPropertyNames(obj).map(
    prop => {
      if (typeof obj[prop] === 'object') {
        freezeRecursively(obj[prop]) // here the function calls itself
      }
    }
  )
}

// So we will make a copy and freeze that using our above function
function freeze (obj) {
  const out = { ...obj }
  
  freezeRecursively(out)
  
  return out
}

// Allowing us to use it like this
const obj = freeze({
  name: 'Bob',
  nicknames: ['J.R.', 'Dobbs'],
  favourites: {
    colour: 'blue',
    number: 7,
    value: 'slack'
  }
})

obj.name = 'Tom'
console.log(`obj.name !== 'Tom': Still '${obj.name}'.`)

obj.nicknames[2] = 'Bobby'
console.log(`obj.nicknames does not include 'Bobby': Still '${obj.nicknames.join(', ')}'.`)

obj.favourites.colour = 'red'
console.log(`obj.favourites.colour !== 'red': Still '${obj.favourites.colour}'. Yay!`)

There are also several libraries for creating immutable objects (e.g., Immutable.js) or for simply freezing objects recursively (e.g., deep-freeze).

This is probably overkill in production. The best way to keep things immutable in JavaScript is with best practices. Learn the techniques and the gotchas and practice them religiously. Use libraries such as deep-freeze (or write your own utility function) in your tests to make sure that your code isn't mutating variables or sharing state.

Can we write a program using only pure functions?

No.

Any function that reaches out for the DOM or uses variables that are not in its scope is impure. But how can our program work if we can't write out to the DOM (or to logs)? We need I/O.

In "pure" functional programming languages, the "impure" I/O is isolated in its own module, as is anything else impure. This permits the bulk of the application to be kept pure. The best practice is to do the same in your code.

The goal of FP is to compose the majority of your program from small pieces of logic that can be combined together and reused. Side effects are inevitable, but by limiting them to certain places in your application, they will be easier to manage and track.

Next, More FP concepts.

Errors, bugs, suggestions, questions? Contact Prateek Sharma.