Why is the raising of an exception a side effect?

Functional ProgrammingSide Effects

Functional Programming Problem Overview


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?

Functional Programming Solutions


Solution 1 - Functional Programming

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.

Solution 2 - Functional Programming

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).

Solution 3 - Functional Programming

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!

Solution 4 - Functional Programming

I realize this is an old question, but the answers here are not wholly correct, IMHO.

Referential transparency refers to the property an expression has if the program it belongs to has the exact same meaning should the expression be replaced by its result. It should be clear that throwing an exception violates referential transparency, and consequently has side-effects. Let me demonstrate why...

I'm using Scala for this example. Consider the following function, which takes an integer argument, i, and adds an integer value, j, to it, then returns the result as an integer. If an exception occurs while adding the two values, then it returns the value 0. Alas, the calculation of j's value results in an exception being thrown (for simplicity, I've replaced j's initialization expression with the resulting exception).

def someCalculation(i: Int): Int = {
  val j: Int = throw new RuntimeException("Something went wrong...")
  try {
    i + j
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

OK. It's a little dumb, but I'm trying to prove a point with a very simple case. ;-)

Let's define and call this function in the Scala REPL and see what we get:

$ scala
Welcome to Scala 2.13.0 (OpenJDK 64-Bit Server VM, Java 11.0.4).
Type in expressions for evaluation. Or try :help.

scala> :paste
// Entering paste mode (ctrl-D to finish)

def someCalculation(i: Int): Int = {
  val j: Int = throw new RuntimeException("Something went wrong...")
  try {
    i + j
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

// Exiting paste mode, now interpreting.

someCalculation: (i: Int)Int

scala> someCalculation(8)
java.lang.RuntimeException: Something went wrong...
  at .someCalculation(<console>:2)
  ... 28 elided    

OK, so obviously, an exception occured. No surprises there.

But remember, an expression is referentially transparent if we can replace it by it's result such that the program has the exact same meaning. In this case, the expression we're focusing on is j. Let's refactor the function and replace j with its result (it's necessary to declare the type of the thrown exception to be an integer, because that is j's type):

def someCalculation(i: Int): Int = {
  try {
    i + ((throw new RuntimeException("Something went wrong...")): Int)
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

Now let's re-evaluate that in the REPL:

scala> :paste
// Entering paste mode (ctrl-D to finish)

def someCalculation(i: Int): Int = {
  try {
    i + ((throw new RuntimeException("Something went wrong...")): Int)
  }
  catch {
    case e: Exception => 0 // Return 0 if we catch any exception.
  }
}

// Exiting paste mode, now interpreting.

someCalculation: (i: Int)Int

scala> someCalculation(8)
res1: Int = 0

Well, I guess you probably saw that coming: we had a different result that time around.

If we calculate j and then attempt to use it in a try block, then the program throws an exception. However, if we just replace j with its value in the block, we get a 0. So throwing exceptions has clearly violated referential transparency.

How should we proceed in a functional manner? By not throwing exceptions. In Scala (there are equivalents in other languages), one solution is to wrap possibly failing results in the Try[T] type: if successful, the result will be a Success[T] wrapping the successful result; if a failure occurs, then the result will be a Failure[Throwable] containing the associated exception; both expressions are sub-types of Try[T].

import scala.util.{Failure, Try}

def someCalculation(i: Int): Try[Int] = {
  val j: Try[Int] = Failure(new RuntimeException("Something went wrong..."))

  // Honoring the initial function, if adding i and j results in an exception, the
  // result is 0, wrapped in a Success. But if we get an error calculating j, then we
  // pass the failure back.
  j.map {validJ =>
    try {
      i + validJ
    }
    catch {
      case e: Exception => 0 // Result of exception when adding i and a valid j.
    }
  }
}

Note: We still use exceptions, we just don't throw them.

Let's try this in the REPL:

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.util.{Failure, Try}

def someCalculation(i: Int): Try[Int] = {
  val j: Try[Int] = Failure(new RuntimeException("Something went wrong..."))

  // Honoring the initial function, if adding i and j results in an exception, the
  // result is 0, wrapped in a Success. But if we get an error calculating j, then we
  // pass the failure back.
  j.map {validJ =>
    try {
      i + validJ
    }
    catch {
      case e: Exception => 0 // Result of exception when adding i and a valid j.
    }
  }
}

// Exiting paste mode, now interpreting.

import scala.util.{Failure, Try}
someCalculation: (i: Int)scala.util.Try[Int]

scala> someCalculation(8)
res2: scala.util.Try[Int] = Failure(java.lang.RuntimeException: Something went wrong...)

This time, if we replace j with its value, we get the exact same result, and that's true in all cases.

However, there's another perspective on this: if the reason that an exception was thrown when calculating the value of j was due to some bad programming on our part (a logic error), then throwing the exception—which would result in terminating the program—may be regarded as an excellent way to bring the problem to our attention. However, if the exception is down to circumstances beyond our immediate control (such as the result of the integer addition overflowing), and we ought to be able to recover from such a condition, then we should formalize that possibility as part of the function's return value, and use, but not throw, an exception.

Solution 5 - Functional Programming

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestioncanadadryView Question on Stackoverflow
Solution 1 - Functional ProgrammingDon StewartView Answer on Stackoverflow
Solution 2 - Functional ProgrammingWoot4MooView Answer on Stackoverflow
Solution 3 - Functional ProgrammingAldo StracquadanioView Answer on Stackoverflow
Solution 4 - Functional ProgrammingMike AllenView Answer on Stackoverflow
Solution 5 - Functional ProgrammingBen SeidelView Answer on Stackoverflow