We’ll start with the most commonly used definition of a pure function, then we’ll provide a few examples of pure and impure functions, with different causes of impurity. Then, we will try to illustrate function purity from a few different angles. Anyway, let’s start with the definition.
Pure function is a function where the return value is:
- only determined by its input values,
- without observable side effects.
If we continue with the pipe analogy, this means that if anything was to enter the pipe must do so at the entrance, as a parameter, and the only thing that leaves does so at the other end, as the return value.
For example addition is a pure function:
function sum(a, b){
return a + b;
}
sum(3, 5);
//=> 9
//we get the result value and nothing else happens
//we call the function with same parameters
sum(3, 5);
//=> 9
//and get the same result
So is our espresso loving example:
function makeEspresso2(sugar, milk){
if(!milk){
return {sugar: sugar, coffee : 1,
milk : 0, water : 1};
}
return {sugar: sugar, coffee : 1,
milk : 0.5, water : 0.5};
}
makeEspresso(2, false);
//=> {sugar: 2, coffee : 1, milk : 0, water : 1}
The only thing these functions depend on are their received parameters and the only effect they produce are their return values - there are no side effects. The pipes have no leaks, in the case of espresso machine quite literally.
Now, what’s the easiest way to violate the first condition for purity? The offending function would need depend on something other than its input parameters. The effect would be that it would return different values with the same inputs.
function now(){
return new Date();
}
//we call it once
now();
//=> Wed Jun 14 2017 19:53:19 GMT+0200 (CEST)
//then again five minutes later...
now();
//=> Wed Jun 14 2017 19:58:19 GMT+0200 (CEST)
You’d expect a function with no parameters to always return the same value. It should be equivalent to a constant.
What happens here is that our function depends on something other than its non-existent parameters - the system clock:
And how would we violate the second condition? Our function will need to somehow change the state of the world around it in a way other than by returning the value. E.g. it can save something to the disk, delete a row from the database, send an email or a text message, make the phone vibrate, etc. Since we want our example to be easily runnable in an average JavaScript shell we’ll settle with a more modest choice - logging to the console:
function printSum(a, b){
let sum = a + b;
console.log('Hey there, the result is: ' + sum);
return sum;
}
printSum(4, 5)
//=> Hey there, the result is: 9
//=> 9
//We do get the return value, but we also get a console message!
To summarize, functions sum
and makeEspresso
are pure because they satisfy both purity conditions. On the
other hand, functions now
and printSum
are not pure since each of them fails to satisfy one of the conditions.
Of course, the function will be just as impure if it violates both conditions, e.g:
function printNow(){
let now = new Date();
console.log(now);
return now;
}
printNow();
//=> Wed Jun 14 2017 19:49:53 GMT+0200 (CEST)
//=> Wed Jun 14 2017 19:49:53 GMT+0200 (CEST)
Another way to explain function purity is with some help of an equivalent concept - referential transparency. A function is said to be referentially transparent if any of its invocations can be replaced with the result value without any change to the program.
For example, we can call the sum
function:
sum(3, 1);
Or we can just type in the result:
4
In the end, it’s a four either way. The two expressions are equivalent.
We can do the same thing with our other pure function - either call it:
makeEspresso(2, false);
Or just type in the result:
{sugar: 2, coffee : 1, milk : 0, water : 1}
Again, the expressions are equivalent.
But, what if we tried the same thing with our impure functions? Let’s try it out with now
function:
now();
Can we replace it with a Date
object literal, e.g. this one:
new Date('2017-06-14T21:06:05')
We can’t or else the time in our application would appear frozen, like a broken clock.
Likewise, we can try the same thing with printSum
:
printSum(3, 2);
Can we replace the function call with its result:
5
We sure can, but the outcome won’t be the same. The literal number 5
does have the same value, but does not
print out Hey there, the result is: 5
on the console. If we carry out the replacement, we’ll miss out any
side effect that the impure function would have caused.
It is clear from the given examples that if a function is referentially transparent it is also pure.
However, we can generalize the idea of replacing function calls with its result even further. We can replace entire functions with combinations of inputs and their respective outputs.
For example, given enough memory our function sum
could be entirely replaced by a lookup table like this:
a | b | sum |
---|---|---|
… | … | … |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 2 |
1 | 2 | 3 |
2 | 1 | 3 |
2 | 2 | 4 |
2 | 3 | 5 |
… | … | … |
And then, instead of actually calculating the sum, we’d only need to look up the result in the table.
Obviously, it would be both memory intensive and impractical to do so in this particular example, as there are way too many combinations there. However, in the cases where the input parameter types are such that the number of possible values is reasonably small, it may make sense to use a lookup table.
One particularly interesting example is the case of pure functions without parameters. As mentioned already, these functions are logically equivalent to constants.
//e.g. this function
function numberOfSecondsInDay(){
return 24*60*60;
}
//is equivalent to
function numberOfSecondsInDay(){
return 86400;
}
//and the whole function can be replaced with a constant
let numberOfSecondsInDay = 86400;