为啥 Haskell 异常只能在 IO monad 中捕获?
Posted
技术标签:
【中文标题】为啥 Haskell 异常只能在 IO monad 中捕获?【英文标题】:Why can Haskell exceptions only be caught inside the IO monad?为什么 Haskell 异常只能在 IO monad 中捕获? 【发布时间】:2011-04-08 06:04:12 【问题描述】:谁能解释为什么异常可能会在 IO monad 之外抛出,但只能在其中捕获?
【问题讨论】:
【参考方案1】:其中一个原因是denotational semantics of Haskell。
(纯)Haskell 函数的一个简洁属性是它们的单调性——更多定义的参数会产生更多定义的值。这个属性非常重要,例如推理递归函数(阅读文章了解原因)。
异常的定义是底部_|_
,它是poset中与给定类型对应的最小元素。因此,为了满足单调性要求,Haskell 函数的任何表示 f
都需要满足以下不等式:
f(_|_) <= f(X)
现在,如果我们可以捕获异常,我们可以通过“识别”底部(捕获异常)并返回更明确的值来打破这种不等式:
f x = case catch (seq x True) (\exception -> False) of
True -> -- there was no exception
undefined
False -> -- there was an exception, return defined value
42
这是完整的工作演示(需要 base-4 Control.Exception):
import Prelude hiding (catch)
import System.IO.Unsafe (unsafePerformIO)
import qualified Control.Exception as E
catch :: a -> (E.SomeException -> a) -> a
catch x h = unsafePerformIO $ E.catch (return $! x) (return . h)
f x = case catch (seq x True) (\exception -> False) of
True -> -- there was no exception
undefined
False -> -- there was an exception, return defined value
42
TomMD 指出,另一个原因是破坏了引用透明度。您可以用相等替换相等的东西并得到另一个答案。 (在指称意义上相等,即它们表示相同的值,而不是在==
意义上。)
我们将如何做到这一点?考虑以下表达式:
let x = x in x
这是一个非终止递归,所以它永远不会返回任何信息,因此也用_|_
表示。如果我们能够捕获异常,我们可以编写函数 f,例如
f undefined = 0
f (let x = x in x) = _|_
(对于严格的函数,后者总是正确的,因为 Haskell 没有提供检测非终止计算的方法——而且原则上也不能,因为 Halting problem。)
【讨论】:
好的。这似乎是 haskell 背后的困难数学背景之一。感谢您的简短描述。 对不起,但这个答案对于我认为是一个非常简单的问题来说完全令人困惑......但这可能更多地反映了我对 Haskell 缺乏了解而不是你的答案。 dodgy_coder:很抱歉,它对您没有用处。可能有不同的角度可以回答这个问题。您可以说您可以(不)这样做,因为相应功能的类型确实(不)允许这样做。那么当然你可能会问为什么这些类型是这样的。答案是“有充分的理由(不)允许这样的事情”,我试图在上面概述这些原因。 谢谢,您提供了一个查看 Haskell 背后数学的地方 @TorosFanny:好点。在我在这里考虑的语义中,这两个概念确实是混淆的(这是习惯性的);但是您当然可以想象它们不在的地方;在这样的语义中,我的论点将是无效的。如果我今天写这个答案,那将是非常不同的:) 主要论点是捕捉异常并不能很好地适应懒惰,因为它取决于评估顺序。【参考方案2】:因为异常可以破坏referential transparency。
您可能在谈论实际上是输入的直接结果的异常。例如:
head [] = error "oh no!" -- this type of exception
head (x:xs) = x
如果您对无法捕获此类错误感到遗憾,那么我向您断言,这些函数不应依赖于 error
或任何其他异常,而应使用正确的返回类型 (Maybe
, @ 987654326@,或者MonadError)。这迫使您以更明确的方式处理异常情况。
与上述情况不同(以及问题背后的原因),异常可能来自信号,例如完全独立于正在计算的值的内存不足情况。这显然不是一个纯粹的概念,必须存在于 IO 中。
【讨论】:
您能举一个以这种方式破坏参照透明性的例子吗? 所以你想告诉我,excpetions 不是从功能上思考的方式,所以我通常应该尽量避免它们?太好了! Roman:我想说的是,如果您可以在纯代码中捕获异常并将结果基于这些异常,那么这样的操作将破坏引用透明度。 啊,我明白了。这可能也是一个问题,您的代码可能会从定义它的函数中逃逸,这也可能会破坏引用透明度。 TomMD:我完全明白你的意思,我只是想看看一个破坏 RT 的具体例子。无论如何,我想出了自己的答案并扩展了我的答案......【参考方案3】:我的解释可能有误,但我是这么理解的。
由于函数在 Haskell 中是纯函数,编译器有权按照他希望的任何顺序对它们求值,并且仍然产生相同的结果。例如,给定函数:
square :: Int -> Int
square x = x * x
表达式square (square 2)
可以用不同的方式求值,但它总是减少到相同的结果,即 16。
如果我们从其他地方调用square
:
test x = if x == 2
then square x
else 0
square x
可以稍后在实际需要值时在 test
函数“外部”进行评估。那时调用堆栈可能与 Java 中的调用堆栈完全不同。
因此,即使我们想捕获square
引发的潜在异常,您应该将catch
部分放在哪里?
【讨论】:
以上是关于为啥 Haskell 异常只能在 IO monad 中捕获?的主要内容,如果未能解决你的问题,请参考以下文章