为什么Haskell异常只能在IO单子内被捕获?

31

有人可以解释一下为什么异常可能会在IO单子之外抛出,但只能在其中捕获吗?

3个回答

26

其中一个原因是Haskell的指称语义

(纯) Haskell函数的一个优良特性是它们的单调性 - 更明确定义的参数会产生更明确定义的值。这个特性非常重要,例如用于推理递归函数(阅读文章以了解其原因)。

异常的指称根据定义是底部_|_,对应于给定类型的偏序集中的最小元素。因此,为满足单调性要求,任何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没有检测非终止计算的手段 -- 而且原则上也不能,因为存在停机问题


1
好的,这似乎是Haskell背后的一些困难数学背景之一。感谢您的简要描述。 - fuz
1
抱歉,但我认为这个回答对于一个非常简单的问题来说完全令人困惑...但这可能更反映了我对Haskell的理解不足,而不是你的回答。 - dodgy_coder
1
不靠谱的程序员:很抱歉这对你没什么用。回答这个问题可能有不同的角度。你可以说你能(不能)这样做是因为相应函数的类型允许(不允许)这样做。当然,你可能会问为什么类型是这样的。答案是“有很好的理由(不)允许这样的事情”,我试图在上面概述这些原因。 - Roman Cheplyaka
谢谢,你提供了一个查看 Haskell 数学背后的地方。 - TorosFanny
但我记得从“http://www.haskell.org/haskellwiki/Error_vs._Exception”引用的“This is unsafe, since Haskell's error is just sugar for undefined”。“Denotation of exception by definition is the bottom”是错误的说法,因为错误和异常是不同的。请您检查一下是否犯了错误。 - TorosFanny
1
@TorosFanny:说得好。在我考虑的语义中,这两个概念确实被混淆了(这是惯例);但你肯定可以想象有一个不混淆它们的语义;在这样的语义中,我的论点将是无效的。如果我今天写这篇答案,它会非常不同 :) 主要的论点将是捕获异常与惰性求值不兼容,因为它取决于求值顺序。 - Roman Cheplyaka

14

因为异常会破坏引用透明性

你可能在谈论那些实际上是由输入直接导致的异常。例如:

head [] = error "oh no!" -- this type of exception
head (x:xs) = x

如果你感到无法捕捉此类错误,那么我向你断言,函数不应该依赖于错误或任何其他异常,而应该使用适当的返回类型(Maybe、Either或者MonadError)。这将强制您以更明确的方式处理异常条件。
与上述不同(导致您提出问题的原因),异常可能来自信号,例如完全独立于正在计算的值的内存不足条件。这显然不是一个纯粹的概念,必须存在于IO中。

2
你能举个例子说明如何打破引用透明性吗? - Roman Cheplyaka
那么你想告诉我,异常不是函数式思考的方式,所以我通常应该尽量避免它们?太好了! - fuz
我的意思是,如果你能在纯代码中捕获异常并基于这些异常来确定结果,这样的操作将会破坏引用透明性。 - Thomas M. DuBuisson
啊,我明白了。也许你的代码可以从定义它的函数中逃脱,这可能会破坏引用透明性,这也是一个问题。 - fuz
TomMD:我完全明白你的意思,我只是想看到一个打破 RT 的具体例子。无论如何,我自己想出了一个并扩展了我的回答... - Roman Cheplyaka

2

我可能在解释上有误,但这就是我的理解。

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部分应该放在哪里呢?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接