24

有人可以解释一下以下 ghci 中的行为与行之间的区别:

catch (return $ head []) $ \(e :: SomeException) -> return "good message"

返回

"*** Exception: Prelude.head: empty list

catch (print $ head []) $ \(e :: SomeException) -> print "good message"

返回

"good message"

为什么第一个案例没有捕获异常?为什么它们不同?为什么第一种情况会在异常消息之前加上双引号?

谢谢。

4

4 回答 4

27

让我们看看第一种情况会发生什么:

catch (return $ head []) $ \(e :: SomeException) -> return "good message"

您创建了 thunk ,它作为一个动作head []被编辑。这个 thunk 不会抛出任何异常,因为它没有被评估,所以整个调用(它是 type )在没有异常的情况下产生 thunk。仅当 ghci 之后尝试打印结果时才会发生异常。如果你试过returnIOcatch (return $ head []) $ ...IO StringString

catch (return $ head []) $ \(e :: SomeException) -> return "good message"
    >> return ()

相反,不会打印任何异常。

这也是你得到 _" * Exception: Prelude.head: empty list_ 的原因。GHCi 开始打印以 . 开头的字符串"。然后它尝试评估字符串,导致异常,并打印出.

尝试替换returnevaluate(将其参数强制为 WHNF)为

catch (evaluate $ head []) $ \(e :: SomeException) -> return "good message"

然后您将强制 thunk 在内部进行评估,catch这将引发异常并让处理程序拦截它。

在另一种情况下

catch (print $ head []) $ \(e :: SomeException) -> print "good message"

当尝试检查时,异常发生在catch零件内部,因此被处理程序捕获。printhead []


更新:正如您所建议的,一件好事是强制该值,最好是其完整的正常形式。这样,您可以确保没有“惊喜”在懒惰的重击中等着您。无论如何,这是一件好事,例如,如果您的线程返回一个未评估的 thunk 并且实际上是在另一个毫无戒心的线程中评估的,您可能会遇到难以发现的问题。

模块Control.Exception已经有evaluate,这会强制插入其 WHNF。我们可以轻松地对其进行扩充以使其达到完整的 NF:

import Control.DeepSeq
import Control.Seq
import Control.Exception
import Control.Monad

toNF :: (NFData a) => a -> IO a
toNF = evaluate . withStrategy rdeepseq

使用它,我们可以创建一个严格的变体catch,强制对其 NF 执行给定的操作:

strictCatch :: (NFData a, Exception e) => IO a -> (e -> IO a) -> IO a
strictCatch = catch . (toNF =<<)

这样,我们可以确定返回的值是完全评估的,所以我们在检查它时不会得到任何异常。您可以验证如果您使用strictCatch而不是catch在第一个示例中,它是否按预期工作。

于 2013-08-05T08:32:24.097 回答
10
return $ head []

包装head []在一个 IO 操作中(因为catch有一个IO类型,否则它将是任何 monad)并返回它。没有捕获任何东西,因为没有错误。head []由于懒惰,此时不会对其本身进行评估,而只会返回。因此,return只添加了一层包装,整个 catch 表达式的结果是head [],非常有效,未计算。只有当 GHCi 或您的程序在稍后的某个时间点实际尝试使用该值时,才会对其进行评估并抛出空列表错误 - 但是,在不同的时间点。

print $ head []

另一方面,立即评估head [],产生随后被捕获的错误。

您还可以看到 GHCi 的区别:

Prelude> :t head []
head [] :: a

Prelude> :t return $ head []
return $ head [] :: Monad m => m a

Prelude> :t print $ head []
print $ head [] :: IO ()

Prelude> return $ head [] -- no error here!

Prelude> print $ head []
*** Exception: Prelude.head: empty list   

为避免这种情况,您也许可以强制该值:

Prelude> let x = head [] in x `seq` return x
*** Exception: Prelude.head: empty list
于 2013-08-05T08:34:55.737 回答
6

GHCi 在IOmonad中工作,并且returnforIO不强制它的论点。所以return $ head []不抛出任何异常,没有抛出的异常也不能被捕获。之后打印结果会引发异常,但此异常不再属于范围catch

于 2013-08-05T08:33:43.227 回答
2

该类型IO a有两个部分:

  • 结构,出去并执行副作用的部分。这由 表示IO
  • 结果,里面保存的纯值这由 表示a

catch函数仅在突破IO结构时捕获异常。

在第一个示例中,您调用print该值。由于打印一个值需要对其执行 I/O,因此在该值中引发的任何异常最终都会出现在结构本身中。所以catch拦截了异常,一切都很好。

另一方面,return不检查其论点。事实上,单子定律保证调用return根本不会影响结构。所以你的价值只是直接通过。

因此,如果您的代码不受异常影响,那么错误消息来自哪里?令人惊讶的是,答案在您的代码之外。GHCi 隐式地尝试print传递给它的每个表达式。但是到那时,我们已经超出了 的范围catch,因此您会看到错误消息。

于 2013-08-18T02:16:06.250 回答