【问题标题】:Why can Haskell exceptions only be caught inside the IO monad?为什么 Haskell 异常只能在 IO monad 中捕获?
【发布时间】:2011-04-08 06:04:12
【问题描述】:

谁能解释为什么异常可能会在 IO monad 之外抛出,但只能在其中捕获?

【问题讨论】:

    标签: haskell exception-handling io monads


    【解决方案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 部分放在哪里?

    【讨论】:

      猜你喜欢
      • 2013-06-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-11
      • 2013-08-15
      • 2017-04-27
      • 2011-07-10
      相关资源
      最近更新 更多