根据维基百科的副作用条目,提出异常构成了副作用。考虑一下这个简单的 python 函数:
def foo(arg): if not arg: raise ValueError('arg cannot be None') else: return 10
使用foo(None)调用它将始终遇到异常。相同的输入,相同的输出。它是参考透明的。为什么这不是一个纯粹的功能?
foo(None)
只有在观察到异常时才会违反纯度,并根据它来更改控制流程做出决定。实际上抛出异常值是引用透明的 - 它在语义上等同于 non-termination 或其他 so-called 底价值。
如果(纯)函数不是总,则它计算为最低值。如何编码底部值取决于实现 - 它可能是一个例外;或 non-termination,或除以零,或其他一些失败。
考虑纯函数:
f :: Int -> Int f 0 = 1 f 1 = 2
没有为所有输入定义。对于一些人,它评估到底部。该实现通过抛出异常对此进行编码。它应该在语义上等同于使用Maybe或Option类型。
Maybe
Option
现在,只有在观察到底部值时才会破坏参照透明度,并根据它做出决策 - 这可能会引入 non-determinism,因为可能抛出许多不同的异常,而您无法知道哪一个。因此,在 Haskell 中捕获异常是在IO monad 中,而生成 so-called “不精确”的例外情况可以纯粹完成。
IO
因此,提出异常本身并不是一个副作用。您是否可以根据异常值修改纯函数的行为 - 从而破坏参照透明度 - 这就是问题所在。
从第一行开始:
“在计算机科学中,如果除了返回一个值之外,它还会修改某个状态或者与调用函数或外部世界有可观察的交互,那么函数或表达式就会产生副作用”
它修改的状态是程序的终止。回答你的另一个问题,为什么它不是一个纯函数。该函数不纯,因为抛出异常会终止程序,因此它会产生副作用(程序结束)。
引发异常可以是纯 OR non-pure,它只取决于引发的异常类型。一个好的 rule-of-thumb 是如果代码引发异常,它是纯粹的,但如果它是由硬件引发的,那么它通常必须被归类为 non-pure。
通过查看硬件引发异常时会发生什么情况可以看出:首先引发中断信号,然后中断处理程序开始执行。这里的问题是中断处理程序不是函数的参数,也不是函数中指定的,而是全局变量。无论何时读取或写入全局变量(也称为状态),您都不再具有纯函数。
将其与您的代码中引发的异常进行比较:您从一组已知的,本地范围的参数或常量构造 Exception 值,然后“抛出”结果。没有使用全局变量。抛出异常的过程本质上是由您的语言提供的语法糖,它不会引入任何 non-deterministic 或 non-pure 行为。正如 Don 所说:“它应该在语义上等同于使用 Maybe 或 Option 类型”,这意味着它应该具有所有相同的属性,包括纯度。
当我说提高硬件异常“通常”被归类为副作用时,并不总是必须如此。例如,如果运行代码的计算机在引发异常时不调用中断,而是将特殊值推送到堆栈,则它不能归类为 non-pure。我相信 IEEE 浮点 NAN 错误是使用特殊值而不是中断抛出的,因此在执行浮点数学时引发的任何异常都可以归类为 side-effect free,因为该值不是从任何全局状态读取的,而是一个常量编码到 FPU 中。
查看片段代码纯粹的所有要求,基于代码的异常和 throw 语句语法糖勾选所有框,它们不修改任何状态,它们与调用函数或调用之外的任何东西没有任何交互,并且它们是引用透明的,但只有在编译器使用您的代码时才会这样。
像所有纯粹的 non-pure 讨论一样,我已经排除了执行时间或内存操作的任何概念,并且假设任何可以纯粹实现的功能都是纯粹地实现而不管其实际实现如何。我也没有证据表明 IEEE 浮点 NAN 异常声明。
引用透明度也可以用计算结果替换计算(e.g. 函数调用),如果函数引发异常,则无法做到这一点。这是因为异常不参与计算,但它们需要被捕获!
4 回答
只有在观察到异常时才会违反纯度,并根据它来更改控制流程做出决定。实际上抛出异常值是引用透明的 - 它在语义上等同于 non-termination 或其他 so-called 底价值。
如果(纯)函数不是总,则它计算为最低值。如何编码底部值取决于实现 - 它可能是一个例外;或 non-termination,或除以零,或其他一些失败。
考虑纯函数:
没有为所有输入定义。对于一些人,它评估到底部。该实现通过抛出异常对此进行编码。它应该在语义上等同于使用
Maybe
或Option
类型。现在,只有在观察到底部值时才会破坏参照透明度,并根据它做出决策 - 这可能会引入 non-determinism,因为可能抛出许多不同的异常,而您无法知道哪一个。因此,在 Haskell 中捕获异常是在
IO
monad 中,而生成 so-called “不精确”的例外情况可以纯粹完成。因此,提出异常本身并不是一个副作用。您是否可以根据异常值修改纯函数的行为 - 从而破坏参照透明度 - 这就是问题所在。
从第一行开始:
它修改的状态是程序的终止。回答你的另一个问题,为什么它不是一个纯函数。该函数不纯,因为抛出异常会终止程序,因此它会产生副作用(程序结束)。
引发异常可以是纯 OR non-pure,它只取决于引发的异常类型。一个好的 rule-of-thumb 是如果代码引发异常,它是纯粹的,但如果它是由硬件引发的,那么它通常必须被归类为 non-pure。
通过查看硬件引发异常时会发生什么情况可以看出:首先引发中断信号,然后中断处理程序开始执行。这里的问题是中断处理程序不是函数的参数,也不是函数中指定的,而是全局变量。无论何时读取或写入全局变量(也称为状态),您都不再具有纯函数。
将其与您的代码中引发的异常进行比较:您从一组已知的,本地范围的参数或常量构造 Exception 值,然后“抛出”结果。没有使用全局变量。抛出异常的过程本质上是由您的语言提供的语法糖,它不会引入任何 non-deterministic 或 non-pure 行为。正如 Don 所说:“它应该在语义上等同于使用 Maybe 或 Option 类型”,这意味着它应该具有所有相同的属性,包括纯度。
当我说提高硬件异常“通常”被归类为副作用时,并不总是必须如此。例如,如果运行代码的计算机在引发异常时不调用中断,而是将特殊值推送到堆栈,则它不能归类为 non-pure。我相信 IEEE 浮点 NAN 错误是使用特殊值而不是中断抛出的,因此在执行浮点数学时引发的任何异常都可以归类为 side-effect free,因为该值不是从任何全局状态读取的,而是一个常量编码到 FPU 中。
查看片段代码纯粹的所有要求,基于代码的异常和 throw 语句语法糖勾选所有框,它们不修改任何状态,它们与调用函数或调用之外的任何东西没有任何交互,并且它们是引用透明的,但只有在编译器使用您的代码时才会这样。
像所有纯粹的 non-pure 讨论一样,我已经排除了执行时间或内存操作的任何概念,并且假设任何可以纯粹实现的功能都是纯粹地实现而不管其实际实现如何。我也没有证据表明 IEEE 浮点 NAN 异常声明。
引用透明度也可以用计算结果替换计算(e.g. 函数调用),如果函数引发异常,则无法做到这一点。这是因为异常不参与计算,但它们需要被捕获!