如何在Haskell中正确使用Control.Exception.catch?

时间:2013-08-05 07:22:42

标签: exception haskell

有人可以解释以下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 个答案:

答案 0 :(得分:25)

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

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

您创建了head []作为return操作的thunk IO。这个thunk不会抛出任何异常,因为它没有被计算,所以整个调用catch (return $ head []) $ ...(类型为IO String)会产生String thunk而没有异常。仅当ghci尝试之后打印结果时才会发生异常。如果你试过

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

相反,不会打印任何例外。

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

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

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

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

在另一种情况下

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

catch尝试检查print时,head []部分内发生异常,因此它被处理程序捕获。


更新:正如您所说,一个好处是强制值,最好是完全正常的形式。通过这种方式,您可以确保在懒惰的thunk中没有“惊喜”等着你。无论如何这是一件好事,例如,如果你的线程返回一个未经评估的thunk并且它实际上是在另一个毫无疑问的线程中进行评估的话,你可能会遇到难以发现的问题。

模块Control.Exception已经有evaluate,这会强制thunk进入其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,则可以按预期工作。

答案 1 :(得分: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

答案 2 :(得分:6)

对于return

GHCi works in the IO monadIO并未强制论证。因此return $ head []不会抛出任何异常,并且无法捕获未抛出的异常。 打印之后的结果会引发异常,但此异常不在catch的范围内。

答案 3 :(得分:2)

类型IO a包含两部分:

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

catch函数只有在突破IO结构时才会捕获异常。

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

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

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