为啥 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 中捕获?的主要内容,如果未能解决你的问题,请参考以下文章

Monads(Haskell)的主要目的[重复]

函数式编程的 Monads,2.2 变体一:异常

Haskell IO 执行顺序

如何在 Haskell 中解析 IO 字符串?

Haskell Monad(下)

在haskell中构建一个非确定性的monad转换器