Haskell中的异常处理

6
如果我理解正确的话,在 Haskell 中,异常主要用于处理 IO monad 中的异常。至少 IO monad 内部的异常可以被捕获。
但是有时候,即使是纯函数也可能会抛出异常,例如当读取的字符串不能表示整数时,read "..." :: Int(),操作符 (!!)(当我们尝试获取列表范围之外的元素)等等。这是真实的行为,我不否认。然而,我不想仅仅为了捕获可能的异常而改变函数签名,因为在这种情况下,我必须在调用栈上更改所有函数的签名。
在 Haskell 中有没有一些模式可以更轻松地处理异常,而不是只能在 IO monad 中处理?也许我应该在这种情况下使用 unsafePerformIO?在纯函数中使用 unsafePerformIO 来捕获异常有多"安全"?

3
我认为这些异常是“你有错误”的类型,因此在第一时间不应该捕获它们。 - CodesInChaos
4个回答

10

在纯代码中,通常最好避免异常的发生。也就是说,除非你绝对确定列表不为空,否则不要使用head,而是使用reads和模式匹配来检查解析错误,而不是使用read

我认为一个好的经验法则是,在纯代码中,异常应该只来自于编程错误,即对error的调用,而这些应该被视为错误,而不是异常处理程序可以处理的内容。

请注意,我只是在谈论纯代码,而在IO中的异常在与“真实世界”进行交互时处理异常情况时有其用途。但是,像MaybeErrorT这样的纯机制更容易使用,因此通常更受欢迎。


1
我强烈反对在Haskell中使用IO中的异常是好的和有用的。你总是最好使用MaybeErrorT或类似的显式方法。这样更容易推理; 异常处理代码似乎总是与错误一起出现。它更具可组合性。它更易于维护。它们应该被禁止。 - John L
@JohnL:嗯,是的,我表达得有点不太对。我已经修改了我的陈述。虽然我不认为它们应该被禁止。但有些东西,比如异步异常(尽管它们很痛苦),不能使用ErrorT等实现。在实践中,我通常会使用ErrorT处理我打算处理的错误,并使用bracket和相关的IO异常函数来清理自己的错误。 - hammar
异步异常确实是一个问题。我应该在我的评论中加上限定词,我只是指同步异常,因为我也没有看到更好的解决方案来处理异步异常。 - John L

5

这就是单子存在的意义!(当然,并不仅限于此,但异常处理是单子范式的一种使用方法)

对于可能失败的函数,您确实需要更改其签名(因为它们会更改语义,您希望在类型中尽可能反映出尽可能多的语义)。但使用这些函数的代码并不必在每个可失败的函数上进行模式匹配;如果他们不关心,他们可以绑定:

head :: [a] -> Maybe a

eqHead :: (Eq a) => [a] -> Maybe [a]
eqHead xs = do
    h <- head xs
    return $ filter (== h) xs

所以,eqHead不能“纯粹”地编写(我希望看到其替代选择的语法选择),但它也不必真正了解headMaybe性质,它只需要知道head可能以某种方式失败。
这并不完美,但是Haskell中的函数与Java没有相同的风格。在典型的Haskell设计中,异常通常不会在调用链深处发生。相反,在我们知道所有参数都已完全定义且行为良好时,深层调用链都是纯的,并且验证发生在最外层。因此,从深处冒出来的异常实际上并不需要在实践中得到支持。
无论是难以实现的设计模式还是缺乏支持此功能的特性引起的,这都是一个有争议的问题。

3
如果你预见到像read这样的函数可能会引发异常,那么为什么不简单地重构你的代码来避免异常的可能性呢?
作为对你问题更直接的回答,有一个叫做spoon的工具。

请注意,spoon确实使用了unsafePerformIO。它不应该也加入一些NOINLINE吗?或者类似的东西? - Dan Burton

3

我要冒昧地反对那些认为"只需一开始避免错误"的人。我建议您将代码结构围绕处理一种单子(monad)内的错误。

纯函数(和FP)的主要优点之一是能够按照“如果一个函数有类型[a] -> a,则对于所有类型为a的列表,将得到类型为a的值”的方式推理您的代码。这类异常会削弱它的优势。

head之所以设置为现在这样,是因为对于初学者来说,在了解Maybe和相似概念之前,学习列表操作更加简单。但如果您理解了更好的方法,我会避免这些风险。


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