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.