Why is the raising of an exception a side effect?
Asked Answered
B

5

54

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?

Bole answered 22/5, 2012 at 13:39 Comment(4)
The exception is not simply returned as a return value.Bethesde
Moving to reopen this is clearly a question, while more theoretical in nature still is valid for this website.Cryoscopy
It should be migrated to Software EngineeringFrame
This seems entirely fine for SO.Johnstone
J
36

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.

Johnstone answered 23/5, 2012 at 12:35 Comment(3)
Interesting, I always thought that the definition of RT was if you could replace an expression with its value, and you cannot have a value of "throws exception A".Breakneck
@JedWesley-Smith You are correct. When a function raises an exception it fails to return a value and has broken referential transparency. The incorrect notation that RT is only broken when the the exception is handled is convenient in that it allows pushing the messy details of fully modeling the domain on to the callers of functions that raise exceptions.Durtschi
Doesn't making decisions based on some bottom values also result in RT (if implemented into the function itself, it could then evaluate to same outputs for same inputs, e.g. through some long catalogue of try/catch statements, which would count for every possible bottom case)?Absalom
C
16

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

Cryoscopy answered 22/5, 2012 at 13:43 Comment(3)
Termination isn't an effect.It isn't state that can be shared. Many pure, partial functions don't terminate, or are only partially defined. They are still pure. Division by zero is a pure function, for example, which fails.Johnstone
Don: Actually, non-termination (aka partiality) often is regarded an effect, especially where people are interested in harvesting the Curry/Howard isomorphism and use types as propositions.Hoof
Ok. I'll buy it if we're distinguishing total and partial functions.Johnstone
E
8

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

Enfeeble answered 15/8, 2019 at 22:7 Comment(0)
N
7

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!

Nevins answered 22/5, 2012 at 13:43 Comment(3)
Well, that may be true if there are no Exceptions. But if there are, you can just throw an Exception instead of executing the function (analog to return a value instead of executing the function).Xavier
Putting exceptions in a try/catch would thus bring purity back to a function?Absalom
It can, it depends on what you do when you handle that exception: if you handle the exception within the function and then return a value within the catch that function can be pure.Nevins
T
5

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.

Template answered 12/5, 2016 at 6:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.