Why is the raising of an exception a side effect?

2019-01-22 01:45发布

问题:

According to the wikipedia entry for side effect, raising an exception constitutes a side effect. Consider this simple python function:

def foo(arg):
    if not arg:
        raise ValueError('arg cannot be None')
    else:
        return 10

Invoking it with foo(None) will always be met with an exception. Same input, same output. It is referentially transparent. Why is this not a pure function?

回答1:

Purity is only violated if you observe the exception, and make a decision based on it that changes the control flow. Actually throwing an exception value is referentially transparent -- it is semantically equivalent to non-termination or other so-called bottom values.

If a (pure) function is not total, then it evaluates to a bottom value. How you encode the bottom value is up to the implementation - it could be an exception; or non-termination, or dividing by zero, or some other failure.

Consider the pure function:

 f :: Int -> Int
 f 0 = 1
 f 1 = 2

This is not defined for all inputs. For some it evaluates to bottom. The implementation encodes this by throwing an exception. It should be semantically equivalent to using a Maybe or Option type.

Now, you only break referential transparency when you observe the bottom value, and make decisions based on it -- which could introduce non-determinism as many different exceptions may be thrown, and you can't know which one. So for this reason catching exceptions is in the IO monad in Haskell, while generating so-called "imprecise" exceptions can be done purely.

So it is just not true that raising an exception is a side effect as such. It is whether or not you can modify the behavior of a pure function based on an exceptional value -- thus breaking referential transparency -- that is the issue.



回答2:

From the first line:

"In computer science, a function or expression is said to have a side effect if, in addition to returning a value, it also modifies some state or has an observable interaction with calling functions or the outside world"

The state it modifies is the termination of the program. To answer your other question about why it is not a pure function. The function is not pure because throwing an exception terminates the program therefore it has a side effect (your program ends).



回答3:

Raising an exception can either be pure OR non-pure, it just depends on the type of exception that is raised. A good rule-of-thumb is if the exception is raised by code, it is pure, but if it is raised by the hardware then it usually must be classed as non-pure.

This can be seen by looking at what occurs when an exception is raised by the hardware: First an interrupt signal is raised, then the interrupt handler starts executing. The issue here is that the interrupt handler was not an argument to your function nor specified in your function, but a global variable. Any time a global variable (aka state) is read or written, you no longer have a pure function.

Compare that to an exception being raised in your code: You construct the Exception value from a set of known, locally scoped arguments or constants, and you "throw" the result. There are no global variables used. The process of throwing an exception is essentially syntactic sugar provided by your language, it does not introduce any non-deterministic or non-pure behaviour. As Don said "It should be semantically equivalent to using a Maybe or Option type", meaning that it should also have all the same properties, including purity.

When I said that raising a hardware exception is "usually" classed as a side effect, it does not always have to be the case. For example, if the computer your code is running on does not call an interrupt when it raises an exception, but instead pushes a special value onto the stack, then it is not classifiable as non-pure. I believe that the IEEE floating point NAN error is thrown using a special value and not an interrupt, so any exceptions raised while doing floating point maths can be classed as side-effect free as the value is not read from any global state, but is a constant encoded into the FPU.

Looking at all the requirements for a piece code to be pure, code based exceptions and throw statement syntactic sugar tick all the boxes, they do not modify any state, they do not have any interaction with their calling functions or anything outside their invocation, and they are referentially transparent, but only once the compiler has had its way with your code.

Like all pure vs non-pure discussions, I have excluded any notion of execution times or memory operations and have operated under the assumption that any function that CAN be implemented purely IS implemented purely regardless of its actual implementation. I also have no evidence of the IEEE Floating point NAN exception claim.



回答4:

Referential transparency is also the possibility to replace a computation (e.g. a function invocation) with the result of the computation itself, something that you can't do if your function raises an exception. That's because exceptions do not take part of computation but they need to be catch!