Pure Functions: Does "No Side Effects" Imply "Always Same Output, Given Same Input"?
Here are a few counterexamples that do not change the outer scope but are still considered impure:
function a() { return Date.now(); }
function b() { return window.globalMutableVar; }
function c() { return document.getElementById("myInput").value; }
-
function d() { return Math.random(); }
(which admittedly does change the PRNG, but is not considered observable)
Accessing non-constant non-local variables is enough to be able to violate the second condition.
I always think of the two conditions for purity as complementary:
- the result evaluation must not have effects on side state
- the evaluation result must not be affected by side state
The term side effect only refers to the first, the function modifying the non-local state. However, sometimes read operations are considered as side effects as well: when they are operations and involve writing as well, even if their primary purpose is to access a value. Examples for that are generating a pseudo-random number that modifies the generator's internal state, reading from an input stream that advances the read position, or reading from an external sensor that involves a "take measurement" command.
The "normal" way of phrasing what a pure function is, is in terms of referential transparency. A function is pure if it is referentially transparent.
Referential Transparency, roughly, means that you can replace the call to the function with its return value or vice versa at any point in the program, without changing the meaning of the program.
So, for example, if C's printf
were referentially transparent, these two programs should have the same meaning:
printf("Hello");
and
5;
and all of the following programs should have the same meaning:
5 + 5;
printf("Hello") + 5;
printf("Hello") + printf("Hello");
Because printf
returns the number of characters written, in this case 5.
It gets even more obvious with void
functions. If I have a function void foo
, then
foo(bar, baz, quux);
should be the same as
;
I.e. since foo
returns nothing, I should be able to replace it with nothing without changing the meaning of the program.
It is clear, then, that neither printf
nor foo
are referentially transparent, and thus neither of them are pure. In fact, a void
function can never be referentially transparent, unless it is a no-op.
I find this definition much easier to handle as the one you gave. It also allows you to apply it at any granularity you want: you can apply it to individual expressions, to functions, to entire programs. It allows you, for example, to talk about a function like this:
func fib(n):
return memo[n] if memo.has_key?(n)
return 1 if n <= 1
return memo[n] = fib(n-1) + fib(n-2)
We can analyze the expressions that make up the function and easily conclude that they are not referentially transparent and thus not pure, since they use a mutable data structure, namely the memo
array. However, we can also look at the function and can see that it is referentially transparent and thus pure. This is sometimes called external purity, i.e. a function that appears pure to the outside world, but is implemented impure internally.
Such functions are still useful, because while impurity infects everything around it, the external pure interface builds a kind of "purity barrier", where the impurity only infects the three lines of the function, but does not leak out into the rest of the program. These three lines are much easier to analyze for correctness than the entire program.