Why is function with useless isolated `static` considered impure?

Viewed 201

In Wikipedia article on Pure function, there is an example of impure function like this:

void f() {
  static int x = 0;
  ++x;
}

With the remark of "because of mutation of a local static variable".

I wonder why is it impure? It's from unit type to unit type, so it always returns the same result for same input. And it has no side effects, because even despite it has static int variable, it's unobservable by any other function than this f(), so there is no observable mutation of global state that other functions might use.

If one argues that any global mutations are disallowed, regardless of whether they are observable or not, then no real life function can be considered pure ever, because any function would allocate its memory on stack, and allocation is impure, as it involves talking to MMU via OS, and allocated page might be residing in a different physical page, and so on, and so on.

So, why does this useless isolated static int makes function impure?

2 Answers

The result of a pure function is fully defined by its input arguments. Here, the result means not only the returned value, but also the effect in terms of the virtual machine defined by the C/C++ standard. In other words, if the function occasionally exhibits undefined behavior with the same input arguments, it cannot be considered pure (because the behavior is different from one call to another with the same input).

In the particular case with the static local variable, that variable may become the source of a data race if f is called concurrently in multiple threads. Data race means undefined behavior. Another possible source of UB is signed integer overflow, which may eventually happen.

The concept of pure functions seems to only matter in... functional languages? Correct me if I'm wrong. The wikipedia link you provide provides two references near the top, one of which is Professor Frisby's Mostly Adequate Guide to Functional Programming. Where there are several different qualifications for a pure function, including:

does not have any observable side effect

This matters because one of the things we can do to a pure function (as opposed to an impure function) is memoization (from the link above), or input/output caching. Pure functions are also testable, reasonable, and self documenting.

I guess memoization matters for the compiler, so asking if a function is "pure" can be considered equivalent to asking if the compiler can memoize the function. It seems like the concept of a static local variable that no other piece of code touches is just bad code, and the compiler should issue a warning about it. But should the compiler optimize it away? And should the compiler try to figure out if any given static local variable actually has no side effects?

It seems like it's just easier to design the compiler to always flag a function is impure if it has a static local, instead of writing logic to hem and haw over whether the function is memoizable or not. See a local static? Boom: no longer pure.

So from a compiler's point of view, it's impure.

What about the other properties of a pure function?

testable, reasonable, and self documenting

Tests are written by a person, usually, so I'd argue this function is testable. Although some automated test-writing software might again see that it's not memoizable, and just choose to ignore writing tests for it entirely. This hypothetical software might just skip anything with local statics. Again, hypothetically.

Is the code reasonable? Certainly not. Although I'm not sure how much this matters. It doesn't do anything. It makes it hard to understand. ("Why did Bob write the function this way? Is this a Magic/More Magic situation?").

Is the code self-documenting? Again, I'd say not. But again, this is degenerate example code.

I think the biggest argument against this being considered a pure function is that a functional language compiler would be perfectly reasonable if it just assumed it wasn't pure.

I think the biggest argument for this being considered a pure function is that we can look at it with our own eyeballs and see that there's obviously no outside behavior. Ignore the fact that signed overflow is undefined. Replace this with a datatype that has defined overflow, and is atomic. Well now there's no undefined behavior, but it still looks weird.

"In conclusion, I don't care whether it's pure or not."

Let me rephrase from my previous (above) conclusion.

I'm inclined to just scan a function for any mutation of static variables and call it a day. Boom, no longer pure.

Can the function be considered pure if we really think about it? Sure. But what's the point? If the definition of a pure function needs to be changed, argue for it to be changed. Seems like you think this is a pure function. That's fine, I see the merits in that. I also see the merits in considering it an impure function.

As much as this is a non-answer, it really depends on what you're using the definition of pure for. If it's writing a compiler? Probably want to use the more conservative definition of pure that allows false positives and excludes this function. If it's to impress a bunch of sophomore CS students while listening to Zep? Go for the definition that recognizes this has no side effects and call it a day.

Related