在 Haskell 中,您可以从纯函数代码中抛出异常,但只能在 IO 代码中捕获。
最佳答案
因为在函数内抛出异常不会使该函数的结果依赖于参数值和函数定义以外的任何东西;函数保持纯净。 OTOH 在函数内部捕获异常确实(或至少可以)使该函数不再是纯函数。
我将研究两种异常(exception)情况。第一个是不确定的;此类异常在运行时不可预测地出现,包括内存不足错误等。这些异常的存在不包含在可能生成它们的函数的含义中。它们只是我们必须处理的生活中令人不快的事实,因为我们在现实世界中拥有实际的物理机器,它们并不总是与我们用来帮助我们对其进行编程的抽象相匹配。
如果函数抛出此类异常,则意味着对该函数求值的特定尝试未能产生值。这并不一定意味着函数的结果是未定义的(在这次调用的参数上),但系统无法产生结果。
如果您可以在纯调用方中捕获这样的异常,您可以做一些事情,例如让一个函数在子计算成功完成时返回一个(非底部)值,并在内存不足时返回另一个值。这作为纯函数没有意义;由函数调用计算的值应该由其参数的值和函数的定义唯一确定。能够根据子计算是否耗尽内存而返回不同的东西,这使得返回值依赖于其他东西(物理机上有多少可用内存、正在运行的其他程序、操作系统及其策略等) .);根据定义,可以以这种方式运行的函数不是纯函数,并且(通常)不能存在于 Haskell 中。
由于纯粹的操作失败,我们确实必须允许评估一个函数可能会产生底部而不是它“应该”产生的值。这并没有完全破坏我们对 Haskell 程序的语义解释,因为我们知道底部会导致所有调用者也产生底部(除非他们不需要应该计算的值,但在这种情况下非-严格评估意味着系统永远不会尝试评估此功能并失败)。这听起来很糟糕,但是当我们将计算放在 IO
monad 中时,我们可以安全地捕获此类异常。 IO
monad 中的值允许依赖于程序“外部”的事物;事实上,它们可以根据世界上的任何东西改变它们的值(这就是为什么 IO
值的一种常见解释是,它们好像传递了整个宇宙的表示)。因此,如果纯子计算内存不足,则 0x2518122231343141 值有一个结果是完全可以的,如果没有,则有另一个结果。
但是确定性异常呢?在这里,我谈论的是在对特定参数集评估特定函数时总是抛出的异常。此类异常包括被零除错误,以及从纯函数显式抛出的任何异常(因为其结果只能取决于其参数及其定义,如果它评估为一次抛出,则它将始终评估为相同的抛出对于相同的论点[1])。
看起来这类异常应该可以在纯代码中捕获。毕竟,IO
的值只是 是 被零除错误。如果一个函数可以有不同的结果取决于子计算是否通过检查它是否传入零来评估为被零除错误,为什么它不能通过检查结果是否是除法来做到这一点-零错误?
在这里,我们回到 larsmans 在评论中提出的观点。如果一个纯函数可以观察它从 1 / 0
得到哪个异常,那么它的结果就取决于执行顺序。但这取决于运行时系统,可以想象它甚至可以在同一系统的两个不同执行之间发生变化。也许我们有一些先进的自动并行化实现,它在每次执行时尝试不同的并行化策略,以便尝试在多次运行中收敛到最佳策略。这将使异常捕获功能的结果取决于所使用的策略、机器中的 CPU 数量、机器上的负载、操作系统及其调度策略等。
同样,纯函数的定义是只有通过其参数(及其定义)进入函数的信息才会影响其结果。在非 throw ex1 + throw ex2
函数的情况下,影响抛出哪个异常的信息不会通过其参数或定义进入函数,因此它不会对结果产生影响。但是 IO
monad 中的计算被允许依赖于整个宇宙的任何细节,因此在那里捕获此类异常很好。
至于您的第二个点:不,其他 monad 不能用于捕获异常。所有相同的论点都适用;产生 IO
或 Maybe x
的计算不应该依赖于它们的参数之外的任何东西,并且捕获任何类型的异常“泄漏”关于那些函数参数中未包含的事物的各种细节。
请记住,monad 没有什么特别之处。它们的工作方式与 Haskell 的其他部分没有任何不同。 monad 类型类是在普通的 Haskell 代码中定义的,几乎所有的 monad 实现也是如此。适用于普通 Haskell 代码的所有相同规则也适用于所有 monad。 [y]
本身很特别,而不是它是一个单子(monad)的事实。
至于其他纯语言如何处理异常捕获,我体验过的唯一具有强制纯洁性的其他语言是 Mercury。 [2] Mercury 的处理方式与 Haskell 略有不同,您可以在纯代码中捕获异常。
Mercury 是一种逻辑编程语言,因此Mercury 程序不是基于函数构建的,而是从谓词构建的;对谓词的调用可以有零个、一个或多个解决方案(如果您熟悉在列表 monad 中编程以获得不确定性,这有点像整个语言都在列表 monad 中)。在操作上,Mercury 执行使用回溯来递归枚举谓词的所有可能的解,但非确定性谓词的语义是它的每组输入参数都有一组解,而不是计算单个的 Haskell 函数每组输入参数的结果值。与 Haskell 一样,Mercury 是纯的(包括 I/O,尽管它使用了略有不同的机制),因此对谓词的每次调用都必须唯一确定一个解决方案集,这仅取决于谓词的参数和定义。
Mercury 跟踪每个谓词的“确定性”。总是只产生一种解决方案的谓词称为 IO
(确定性的缩写)。那些产生至少一种解决方案的称为 det
。还有一些其他确定性类,但它们在这里不相关。
使用 multi
块捕获异常(或通过显式调用实现它的高阶谓词)具有确定性 try
。 cc 代表“ promise 的选择”。这意味着“这个计算至少有一个解决方案,并且在操作上程序只会得到其中一个”。这是因为运行子计算并查看它是否产生异常有一个解决方案集,它是子计算的“正常”解决方案加上它可能抛出的所有可能异常的集合。由于“所有可能的异常”包括所有可能的运行时故障,其中大部分永远不会真正发生,因此无法完全实现此解决方案集。执行引擎实际上不可能通过所有可能的解决方案回溯到 cc_multi
块,所以它只是给你 一个 解决方案(或者是一个正常的解决方案,或者探索一个没有所有可能解决方案的异常迹象) ,或第一个发生的异常)。
因为编译器会跟踪确定性,所以它不允许您在完整解决方案集很重要的上下文中调用 try
。例如,您不能使用它来生成没有遇到异常的所有解决方案,因为编译器会提示它需要 try
调用的所有解决方案,而这只会产生一个。但是,您也不能从 cc_multi
谓词调用它,因为编译器会提示 det
谓词(应该只有一个解决方案)正在执行 det
调用,它只会有多个解决方案(知道其中之一是什么)。
那么这到底有什么用呢?好吧,你可以将 cc_multi
(以及它调用的其他东西,如果有用的话)声明为 main
,他们可以毫无问题地调用 cc_multi
。这意味着整个程序理论上有多个“解决方案”,但运行它会生成 一个 解决方案。这允许您编写一个程序,当它碰巧在某个时刻用完内存时,它的行为会有所不同。但它不会破坏声明式语义,因为它会用更多可用内存计算出的“真实”结果仍在解决方案集中(就像程序实际执行时内存不足异常仍在解决方案集中一样计算一个值),只是我们最终只能得到一个任意的解决方案。
重要的是 try
(只有一种解决方案)与 det
(有多种解决方案,但您只能拥有其中一种)的处理方式不同。类似于在 Haskell 中捕获异常的推理,异常捕获不能发生在非“提交选择”的上下文中,或者您可以获得纯谓词,根据来自现实世界的信息,它们应该产生不同的解决方案集。 t有权访问。 cc_multi
的 cc_multi
确定性允许我们编写程序,就好像它们产生了一个无限的解决方案集(主要是不太可能的异常的小变种),并阻止我们编写实际上需要多个解决方案的程序。 [3]
[1] 除非评估它首先遇到不确定性错误。现实生活很痛苦。
[2] 仅仅鼓励程序员使用纯度而不强制它的语言(例如 Scala)往往只是让您在任何地方捕获异常,就像它们允许您在任何地方进行 I/O 一样。
[3] 请注意,“ promise 的选择”概念并不是 Mercury 处理纯 I/O 的方式。为此,Mercury 使用独特的类型,它与“ promise 选择”确定性类正交。
关于haskell - 为什么捕获异常是非纯的,而抛出异常是纯的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12335245/