The “Pure” in Pure Functions

Chidume Nnamdi 🔥💻🎵🎮
Dev Proto
Published in
6 min readJul 30, 2020

Pure functions affect/change data/variables that exist only on its function scope. Pure functions are idempotent. The return value of pure functions depends on its inputs/arguments. Pure functions do not use data/variables that exist outside its function scope.

All these make up the “Pure” in pure functions. In this post, we will expand and elaborate on each characteristic:

Alert!: Check out this new REST/GraphQL testing tool:

Side-Effects

Side-effects happen when the global variables, static variables, and variables in the outside scope are changed or modified in any way.

var count = 90function sideEffects(a, b) {
return a * b + count++;
}

See, the sideEffects function increments the count variable. The count variable does not exist within its function scope. It is a global variable in the global scope. Being changed inside the sideEffects function makes it a side-effectin function, and an impure function.

function impureFn(a, b = {}) {
b.count++
return a * b.count
}

This impureFn function is an impure function. It is so because it changes the count property in the b object, the bobject will hold a reference to the original object passed so it will be affected. It will result in the function modify a variable from its outer scope.

function pureFn(a, b) {
let b = Math.PI
let res = b * b
return a * res
}

pureFn is a pure function. It affects no data/variables from an outside scope, everything is contained within the function scope.

Side-effects also occur when network request is made, events and Observables subscription.

Idempotent

Idempotency is getting the same result with the same inputs occurring the same.

function nonIdempotent(a) {
return Math.ceil(Math.random() * a)
}

This function generates a random number whenever called and multiply it by the a argument.

If we call the function with param 4 numerous times:

nonIdempotent(4) // 2
nonIdempotent(4) // 4
nonIdempotent(4) // 3
nonIdempotent(4) // 1

The outputs are different on each call of the nonIdempotent function. Call the function again, you will also see that the output varies.

This function is not idempotent.

Let’s see another function:

function idemFn(a) {
return b * 8
}

Let’s call this function with value 4 numerous times.

idemFn(4) // 32
idemFn(4) // 32
idemFn(4) // 32
idemFn(4) // 32

See that the function returns the same output 32 so far as the input is 4 no matter how many times the function is called with argument 4.

This function is idempotent.

Output dependency on Argument

One thing to note here is that pure functions must have at least one argument, and they must return a value.

So with that, the outputs must depend on its arguments. This means that the return value must have an influence from the argument or calculated from the arguments.

function nonInde(a) {
log(a)
return Math.PI * Math.random()
}

See, the return value of nonIde is not dependent on the argument a.

function indeArg(a) {
return Math.PI * a
}

Here, the return value of the indeArg function is determined by the value of the a argument.

Non-usage of variables outside its scope

Pure functions should not use or refer to global variables, static variables, or variables outside its function scope.

var gv = 90function fn(a) {
return a * gv
}

The fn function used the gv variable to determine the return value. With this, we will term the fn function an impure function.

Now, let’s look at the benefits of pure functions and why pure functions should obey the above-listed rules:

Predictability

Being predictable is that the pure function does not side-effect and we can foresay the return value.

If a function side-effects, to predict its return value will be difficult because we don’t know if the variable has been changed by another source.

var b = 1function impure(a) {
return a * b
}

This function refers to a global variable b to multiply it by the argument a.

Now, if we pass 4, we know the return value will be 4. arg 6, and the return value will be 6.

Yes, it seems to be predictable, but no, it’s not predictable. The function uses the global variable b. If the global variable has been modified from another scope, then our prediction of, arg 6 to return 6, will fail.

See this:

var b = 1function impure(a) {
return a * b
}
function chgFn() {
return b++ * Date.now();
}

chgFn increments the b variable. If the chgFn is called before impure(6), then the result of the impure(6) will be 12. If the chgFn is called twice, the b will be modified to 3, so impure(6) will return 18.

Do you see the unpredictability?

Now, if we have a pure function that accesses no outside variable, the function will be predictable because it depends on no outside var, everything is contained within.

function pure(a) {
return a * 10
}

We can predict the return value of the pure function given any argument. For sure, arg 4 will return 40. arg 6 will return 60.

Optimizable

Once a function is predictable it can be optimized via memoization. This memoization is a process whereby the results of an op in a function are cached based on the inputs and the cache is returned when the same inputs occur again.

This memoization is effective in making the function skip its op and return the cache results instead. This will seriously have a huge impact on performance if the function is expensive.

To memoize a function, we store the return value in a cache map with the input as key and return the return value. Then next time the same input is passed to the function, we get the value stored in the cache map and return it:

function memoizeFn(a) {
memoizeFn.cache = memoizeFn.cache || new Map()
if(memmoizeFn.cache.get(a))
return memmoizeFn.cache.get(a)
else {
let res = a * 100
memmoizeFn.cache.set(a, res)
return res
}
}

For every, call to the function, it checks if the arg a is stored in the cache Map as key, if true it returns the value of the a key. If not, it performs the mul op and saves the result to the cache Map with a arg as key and the return value a value, then the return value is returned.

Let’s say we have a function that refers to a global var:

var b = 1function impure(a) {
return a * b
}

Now, it is not predictable. If we memoize the inputs of this function, it will result in stale data. Why?

Look at this. If impure is called with 4, we know that 4 will be returned and the result will be cached. Now, if another function or factor modifies b, let’s say to 5. If impure is called with arg 4 again, the cached results will be 4. Which shouldn’t be so because b has changed.

So you see why pure functions must be self-contained without any reference to an outside variable.

A pure function has no access or refers to external data. It makes it ideal to be cached because it is predictable and no external influence, so there is no need to recalculate the return value every time for an input if we can foresay the result, so we can store the return value with pinter to the input, and return it when the function is called with the input.

Conclusion

We have seen what the “pure” in pure functions actually mean. They are clearly self-contained, independence and have no biz with outside variables. Just like Wakanda, they are not open to the outside, thus making Wakanda pure, devoid of any foreign body or contact, religion or ideology. This makes them the same everywhere.

If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email or DM me.

Thanks !!!

Enjoyed this article? Get more of it by filling out the form:

--

--

Chidume Nnamdi 🔥💻🎵🎮
Dev Proto

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕