Haskell异常的含义

28
在 Haskell 中,异常的含义是什么?我唯一看到的用法就是在我的代码中放置 undefinederror 以阻止程序运行。否则,我认为使用异常编程是一个逻辑设计缺陷。但是 Haskell 有一个高级异常模块 Control.Exception,它被 Prelude 使用。我读到 C++ 中使用异常的原因是为了防止代码中出现大量的“调用函数,然后检查状态”行。但这些东西可以在 Haskell 中抽象出来。我能看到的 Haskell 中处理异常的另一个原因只能是 FFI 中处理外部异常,但仅适用于 Haskell 函数内部包装调用的情况。

有各种各样的原因会导致异常。在一个无异常的系统中,当发生以下情况时会发生什么:内存不足?操作系统发送信号?除以零?评估部分函数...并发现它是部分的!我知道异常不是任何一种语言中美好的部分,但为了应对现实世界,它们是必要的一部分。 - Thomas M. DuBuisson
4
这个问题是否有真正的答案,还是只是在抱怨?如果只是在抱怨,或许应该用博客文章的形式更为合适。 - Daniel Wagner
1
是的,对于这种情况应该有可能停止程序,因此我们有如我所说的“未定义”和“错误”。除以零和在函数“域外”进行评估都是逻辑设计缺陷。但我不认为在程序机制(在Haskell中)中使用异常的意义。 - telephone
1
丹尼尔:这不是抱怨。我从《Real World Haskell》学习了Haskell,他们写了关于异常的内容。但我后来从未使用过它,所以我想知道在编程Haskell时是否实际上需要使用异常。在我目前的看法中,它是不可用的。 - telephone
7
即使这是一段发泄,它仍然是值得在SO上讨论的话题。在我看来,与那些新手问题(比如:“是否存在一个函数IO a -> a?”)相比,这个问题明显增加了价值。 - fuz
显示剩余3条评论
5个回答

27

依我之见,异常意味着“您违反了函数的契约”。 我所说的并不是类型合同,而是通常在注释中找到的内容。

-- The reciprocal is not defined for the value 0
myRecip :: Fractional a => a -> a
myRecip x | x == 0    = throw DivideByZero
          | otherwise = 1 / x

当然,您始终可以以“安全”的方式提供这些函数:

safeRecip :: Fractional a => a -> Maybe a
safeRecip x | x == 0    = Nothing
            | otherwise = Just $ 1 / x

或许我们甚至应该将这个模式抽象化

restrictInput :: (a -> Bool) -> (a -> b) -> (a -> Maybe b)
restrictInput pred f x = if pred x then Just (f x) else Nothing

safeRecip' = restrictInput (/= 0) myRecip

你可以想象类似的组合器来使用Either而不是Maybe来报告失败。既然我们有能力使用纯函数处理这些事情,为什么还要使用不纯的异常模型呢?好吧,大多数Haskeller会告诉你坚持纯度。但真相是,添加额外的层级很麻烦。你不能再写

prop_recip x = x == (recip . recip) x
因为现在recip x的结果是一个Maybe。你可以做一些有用的事情,使用 (a -> a) 函数来做你以前无法做的事情。现在你必须考虑组合Maybes。如果你熟悉单子,这当然是微不足道的:
prop_recip 0 = (safeRecip >=> safeRecip) 0 == Nothing
prop_recip x = (safeRecip >=> safeRecip) x == Just x

但问题在于这里。新手在单子组合方面通常几乎什么都不知道。正如许多#haskell irc频道上的人会迅速告诉您的那样,Haskell委员会在语言设计方面做出了一些相当奇怪的决定,以迎合新手。我们希望能够说“您无需了解单子即可开始在Haskell中制作有用的东西”。我基本上同意那种看法。

tl;dr 对问题“What is an exception?”进行快速回答:

  • 一个肮脏的hack,因此我们不必使函数完全安全
  • IO单子的“try/catch”控制流机制(IO是一个罪恶之地,那么为什么不在罪恶清单中添加throw try/catch呢?)

还可能有其他解释。

另请参见Haskell Report > Basic Input/Output > Exception Handling in the IO Monad

*我实际上在#haskell irc上询问了他们是否赞成这个说法。我唯一得到的回复是"wonky" :),所以很明显通过反对证明了这个说法是正确的。


[编辑]请注意,errorundefinedthrow为基础定义:

error :: [Char] -> a
error s = throw (ErrorCall s)

undefined :: a
undefined =  error "Prelude.undefined"

3
在倒数函数中使用除 'error' 之外的其他异常是我认为异常使用的不好。该函数永远不应该被传入该值,程序也永远不应尝试捕获来自该函数的异常,如果这样做,我们的程序就存在逻辑缺陷。在这种情况下,我更喜欢使用 'error' 来终止程序,以便检测到错误。如果以异常处理的方式悄悄地处理bug,可能会导致后续出现其他bug。 - telephone
3
@telephone 使用error并不是什么神奇的事情:你可以捕获抛出的ErrorCall异常并在不引起骚动的情况下处理它。 let foo a = error "rawr" in Control.Exception.Base.catch (foo ()) (\(ErrorCall msg) -> putStrLn msg)会打印出消息"rawr"。 - Dan Burton
澄清一下:我同意默默处理异常是相当糟糕的做法。我只是在说error本质上无非是(throw . ErrorCall)这个宏,所以我不同意使用除了'error'之外的异常是错误的。只要你不捕获异常,你可以用任何老旧的异常来终止程序;ErrorCall并没有什么不同。然而,关于ErrorCall(以及它的便利函数error)的好处在于,它使得你可以简单地将错误消息写成字符串,而不需要费事地创建自己的异常类型。 - Dan Burton
+1表示“显然已经被证明是正确的,因为没有反对意见。”让我笑了。 - wsanville

5
"error"函数用于处理无效输入或发生不应该发生的内部错误(即Bug)。简而言之,调用“error”表示存在Bug——不管是在调用方还是被调用方。
"undefined"常量更多地用于不应该使用的值,通常是因为它们将被其他内容替换,或者因为它们是用于获取特定类型的幽灵值。(实际上,它的实现就是对"error"的调用。)
那么为什么我们会有所有这些花哨的Control.Exception呢?
基本上,“因为IO操作可能会抛出异常”。你可能正在通过TCP套接字愉快地与FTP服务器交流,然后突然连接断开。结果是什么?你的程序抛出了一个异常。或者你的RAM用完了,或者磁盘满了,或者其他什么原因。
注意,几乎所有这些情况都不是你的错。如果你能预见到某个具体的问题会出现,你应该使用诸如MaybeEither之类的东西以纯粹的方式进行处理。(例如,如果你要反转矩阵,那么矩阵可能是不可逆的,所以你最好返回一个Maybe Matrix作为结果。)对于你无法合理预见的事情(例如,一些其他程序刚刚删除了你正在处理的文件),异常是唯一的选择。
请注意,Control.Exception不仅包含用于定义各种不同类型的东西,还包含大量用于处理异常的内容。我不在乎我调用的代码是否执行了错误操作,因此存在Bug;我仍然希望能够告诉我刚才通信的客户端连接即将关闭,记录一些描述信息到某个日志文件,并进行其他清理工作,而不是让我的程序突然停止。"


1

异常是一种合法的流程控制形式。对我来说不清楚的是,当程序员拥有一个工具时,为什么会坚持认为它“只适用于”某些情况并排除其他可能的用途。

例如,如果您正在执行回溯计算,可以使用异常进行回溯。在Haskell中,使用列表单子可能更常见,但异常也是一种合法的方法。


0

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