根据维基百科side effect条目,提出异常构成副作用。考虑一下这个简单的python函数:
def foo(arg):
if not arg:
raise ValueError('arg cannot be None')
else:
return 10
使用foo(None)
调用它将始终遇到异常。相同的输入,相同的输出。它是参考透明的。为什么这不是一个纯粹的功能?
答案 0 :(得分:32)
只有在观察到异常时才会违反纯度,并根据它更改控制流程做出决定。实际上抛出异常值是引用透明的 - 它在语义上等同于非终止或其他所谓的bottom values.
如果(纯)函数不是total,则计算结果为最低值。如何编码底部值取决于实现 - 它可能是一个例外;或非终止,或除以零,或其他一些失败。
考虑纯函数:
f :: Int -> Int
f 0 = 1
f 1 = 2
没有为所有输入定义。对于一些人,它评估到底部。该实现通过抛出异常对此进行编码。它应该在语义上等同于使用Maybe
或Option
类型。
现在,只有在观察底部值时才会破坏引用透明度,并根据它做出决策 - 这可能会引入非确定性,因为可能抛出许多不同的异常,并且你可以'我知道哪一个。因此,因此在Haskell中IO
monad中捕获异常,而生成所谓的"imprecise" exceptions可以完全实现。
因此提出异常是一种副作用是不正确的。是否可以根据异常值修改纯函数的行为 - 从而破坏参照透明度 - 这就是问题所在。
答案 1 :(得分:11)
从第一行开始:
“在计算机科学中,一个函数或表达被认为有一个方面 效果如果,除了返回一个值,它还会修改一些 状态或与调用函数或者具有可观察的交互 外面的世界“
它修改的状态是程序的终止。回答你的另一个问题,为什么它不是一个纯函数。该函数不纯,因为抛出异常会终止程序,因此它会产生副作用(程序结束)。
答案 2 :(得分:5)
引发异常可以是纯粹的或非纯粹的,它只取决于引发的异常类型。一个好的经验法则是,如果代码引发异常,它是纯粹的,但如果它是由硬件引发的,那么它通常必须被归类为非纯粹的。
通过查看硬件引发异常时会发生什么情况可以看出:首先引发中断信号,然后中断处理程序开始执行。这里的问题是中断处理程序不是函数的参数,也不是函数中指定的,而是全局变量。无论何时读取或写入全局变量(也称为状态),您都不再具有纯函数。
将其与代码中引发的异常进行比较:从一组已知的本地范围的参数或常量构造Exception值,然后“抛出”结果。没有使用全局变量。抛出异常的过程本质上是由您的语言提供的语法糖,它不会引入任何非确定性或非纯粹的行为。正如Don所说:“它应该在语义上等同于使用Maybe或Option类型”,这意味着它应该具有所有相同的属性,包括纯度。
当我说提出硬件异常“通常”被归类为副作用时,并不总是必须如此。例如,如果运行代码的计算机在引发异常时不调用中断,而是将特殊值推送到堆栈,则它不能归类为非纯类。我相信IEEE浮点NAN错误是使用特殊值而不是中断引发的,因此在执行浮点数学时引发的任何异常都可以归类为副作用,因为该值不是从任何全局状态读取的,而是常量编码到FPU中。
查看片段代码纯粹的所有要求,基于代码的异常和throw语句语法糖勾选所有框,它们不修改任何状态,它们与其调用函数或其他任何东西之外没有任何交互调用,并且它们是引用透明的,但只有在编译器已经使用您的代码时才会是透明的。
与所有纯粹与非纯粹的讨论一样,我已经排除了执行时间或内存操作的任何概念,并且假设任何可以纯粹实现的功能都是纯粹地实现,而不管其实际实现如何。我也没有证据表明IEEE浮点NAN异常声明。
答案 3 :(得分:4)
参照透明度也可以用计算结果替换计算(例如函数调用),如果函数引发异常,则无法做到这一点。那是因为异常不参与计算,但需要抓住它们!
答案 4 :(得分:4)
我意识到这是一个古老的问题,但是恕我直言,这里的答案并不完全正确。
引用透明性是指表达式所具有的属性,如果表达式所属的程序具有完全相同的含义,则该表达式应被其结果替换。应当明确的是,引发异常会违反参照透明性,因此具有副作用。让我来说明为什么...
在此示例中,我使用的是 Scala 。考虑以下函数,该函数采用整数参数i
,并向其添加整数值j
,然后以整数形式返回结果。如果在将两个值相加时发生异常,那么它将返回值0。可惜,j
值的计算会引发异常(为简单起见,我替换了j
'的初始化表达式以及产生的异常)。
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.
}
}
好。这有点愚蠢,但我想用一个非常简单的案例来证明这一点。 ;-)
让我们在 Scala REPL 中定义并调用此函数,然后看看我们得到了什么:
$ 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
好的,很明显,发生了异常。没有惊喜。
但是请记住,如果我们可以将表达式替换为结果,以使程序具有完全相同的含义,则该表达式是相对透明的。在这种情况下,我们关注的表达式是j
。让我们重构函数并用其结果替换j
(必须将抛出的异常的类型声明为整数,因为它是j
的类型):
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.
}
}
现在让我们在 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
好吧,我想您可能已经看到了:那个时候我们得到了不同的结果。
如果我们计算j
,然后尝试在try
块中使用它,则程序将引发异常。但是,如果仅用块中的值替换j
,则会得到0。因此抛出异常显然违反了引用透明性。
我们应该如何以功能的方式进行?通过不抛出异常。在 Scala 中(其他语言中的等效项),一种解决方案是将可能失败的结果包装为Try[T]
类型:如果成功,则结果将为Success[T]
包装成功的结果如果发生故障,那么结果将是包含相关异常的Failure[Throwable]
;这两个表达式都是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.
}
}
}
注意:我们仍然使用异常,只是不抛出异常。
让我们在 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...)
这一次,如果我们将j
替换为它的值,则会得到完全相同的结果,在所有情况下都是如此。
但是,对此还有另一种观点:如果计算j
的值时引发异常的原因是由于我们这一方面的编程错误(逻辑错误) ,然后引发异常(这将导致程序终止),可以被视为将问题引起我们注意的绝佳方法。但是,如果异常是由于我们无法直接控制的情况(例如整数加法溢出的结果),并且我们应该能够从这种情况中恢复过来,那么我们应该将该可能性形式化,作为函数返回的一部分值,并使用但不抛出异常。