Most sources define a pure function as having the following two properties:

  1. Its return value is the same for the same arguments.
  2. Its evaluation has no side effects.

It is the first condition that concerns me. In most cases, it's easy to judge. Consider the following JavaScript functions (as shown in this article)

Pure:

const add = (x, y) => x + y;

add(2, 4); // 6

Impure:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

It's easy to see that the 2nd function will give different outputs for subsequent calls, thereby violating the first condition. And hence, it's impure.

This part I get.


Now, for my question, consider this function which converts a given amount in dollars to euros:

(EDIT - Using const in the first line. Used let earlier inadvertently.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Assume we fetch the exchange rate from a db and it changes every day.

Now, no matter how many times I call this function today, it will give me the same output for the input 100. However, it might give me a different output tomorrow. I'm not sure if this violates the first condition or not.

IOW, the function itself doesn't contain any logic to mutate the input, but it relies on an external constant that might change in the future. In this case, it's absolutely certain it will change daily. In other cases, it might happen; it might not.

Can we call such functions pure functions. If the answer is NO, how then can we refactor it to be one?


Solution 1:

The dollarToEuro's return value depends on an outside variable that is not an argument; therefore, the function is impure.

If the answer is NO, how then can we refactor the function to be pure?

One option is to pass in exchangeRate. This way, every time arguments are (something, somethingElse), the output is guaranteed to be something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Note that for functional programming, you should avoid let - always use const to avoid reassignment.

Solution 2:

Technically, any program that you execute on a computer is impure because it eventually compiles down to instructions like “move this value into eax” and “add this value to the contents of eax”, which are impure. That's not very helpful.

Instead, we think about purity using black boxes. If some code always produces the same outputs when given the same inputs then it's considered to be pure. By this definition, the following function is also pure even though internally it uses an impure memo table.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

We don't care about the internals because we are using a black box methodology for checking for purity. Similarly, we don't care that all code is eventually converted to impure machine instructions because we're thinking about purity using a black box methodology. Internals are not important.

Now, consider the following function.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

Is the greet function pure or impure? By our black box methodology, if we give it the same input (e.g. World) then it always prints the same output to the screen (i.e. Hello World!). In that sense, isn't it pure? No, it's not. The reason it's not pure is because we consider printing something to the screen a side effect. If our black box produces side effects then it is not pure.

What is a side effect? This is where the concept of referential transparency is useful. If a function is referentially transparent then we can always replace applications of that function with their results. Note that this is not the same as function inlining.

In function inlining, we replace applications of a function with the body of the function without altering the semantics of the program. However, a referentially transparent function can always be replaced with its return value without altering the semantics of the program. Consider the following example.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Here, we inlined the definition of greet and it didn't change the semantics of the program.

Now, consider the following program.

undefined;
undefined;

Here, we replaced the applications of the greet function with their return values and it did change the semantics of the program. We are no longer printing greetings to the screen. That's the reason why printing is considered a side effect, and that's why the greet function is impure. It's not referentially transparent.

Now, let's consider another example. Consider the following program.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Clearly, the main function is impure. However, is the timeDiff function pure or impure? Although it depends upon serverTime which comes from an impure network call, it is still referentially transparent because it returns the same outputs for the same inputs and because it doesn't have any side effects.

zerkms will probably disagree with me on this point. In his answer, he said that the dollarToEuro function in the following example is impure because “it depends upon the IO transitively.”

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

I have to disagree with him because the fact that the exchangeRate came from a database is irrelevant. It's an internal detail and our black box methodology for determining the purity of a function doesn't care about internal details.

In purely functional languages like Haskell, we have an escape hatch for executing arbitrary IO effects. It's called unsafePerformIO, and as the name implies if you do not use it correctly then it's not safe because it might break referential transparency. However, if you do know what you're doing then it's perfectly safe to use.

It's generally used for loading data from configuration files near the beginning of the program. Loading data from config files is an impure IO operation. However, we don't want to be burdened by passing the data as inputs to every function. Hence, if we use unsafePerformIO then we can load the data at the top level and all our pure functions can depend upon the immutable global config data.

Note that just because a function depends upon some data loaded from a config file, a database, or a network call, doesn't mean that the function is impure.

However, let's consider your original example which has different semantics.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Here, I'm assuming that because exchangeRate is not defined as const, it's going to be modified while the program is running. If that's the case then dollarToEuro is definitely an impure function because when the exchangeRate is modified, it'll break referential transparency.

However, if the exchangeRate variable is not modified and will never be modified in the future (i.e. if it's a constant value), then even though it's defined as let, it won't break referential transparency. In that case, dollarToEuro is indeed a pure function.

Note that the value of exchangeRate can change every time you run the program again and it won't break referential transparency. It only breaks referential transparency if it changes while the program is running.

For example, if you run my timeDiff example multiple times then you'll get different values for serverTime and therefore different results. However, because the value of serverTime never changes while the program is running, the timeDiff function is pure.