正确的Haskell异常处理设计

12

我目前正在努力理解如何在Haskell中正确使用异常。 异常的工作方式足够简单; 我试图清楚地了解正确解释它们的方式。

基本立场是,在设计良好的应用程序中,异常不应该逃逸到顶层。 任何发生这种情况的异常显然都是设计者没有预料到的 - 即程序错误(例如除以零),而不是不寻常的运行时事件(例如文件未找到)。

为此,我编写了一个简单的顶级异常处理程序,它捕获所有异常并向stderr打印一条消息,指示“这是一个错误”(然后重新抛出异常以终止程序)。

但是,假设用户按下Ctrl + C。 这会导致抛出异常。 明显,这不是任何程序错误。 但是,未能“预料并对此进行反应”的用户中止可能被视为错误。 因此,程序应该捕获它并适当处理它,在退出之前执行任何必要的清理。

问题在于... 处理此代码将捕获异常,释放任何资源或其他内容,然后重新抛出异常!因此,如果异常到达顶层,这并不一定意味着它未经处理。 它只是意味着我们想要快速退出。

那么,我的问题是:应该使用异常来控制流程吗?每个明确捕获UserInterrupt的函数都应该使用显式的流控制结构手动退出而不是重新抛出异常吗? (但是调用者如何知道也要退出?)UserInterrupt到达顶层是否可以接受? 但在这种情况下,按同样的论点,ThreadKilled是否可以接受?

简而言之,中断处理程序是否应该为UserInterrupt(可能还有ThreadKilled)做特殊处理?HeapOverflowStackOverflow呢?那个是一个错误吗?还是"程序无法控制的情况"?

2个回答

7

在异常情况下进行清理

然而,未能预见和对用户中止等情况作出反应可能被视为一个漏洞。因此,程序应该捕获这种异常并在退出前适当地处理它,执行任何必要的清理工作。

某种意义上来说,您是正确的 - 程序员应该预见到异常情况。但不是通过捕获它们。相反,您应该使用异常安全的函数,例如 bracket。 例如:

import Control.Exception

data Resource

acquireResource :: IO Resource
releaseResource :: Resource -> IO ()

workWithResource = bracket acquireResource releaseResource $ \resource -> ...

这样做的好处是,无论程序是否被Ctrl+C中止,资源都将得到清理。

异常是否应该达到顶级目录?

现在,我想谈谈你的另一个说法:

基本立场是,在设计良好的应用程序中,异常不应该逃逸到顶级目录。

我认为,在设计良好的应用程序中,异常是一种完全可以终止程序的方式。如果有任何问题,那么你正在做某些错误的事情(例如,想在main结束时执行清理操作--但这应该在bracket中完成!)。

下面是我经常在我的程序中做的事情:

  1. Define a data type that represents any possible error — anything that might go wrong. Some of them often wrap other exceptions.

    data ProgramError
      = InputFileNotFound FilePath IOException
      | ParseError FilePath String
      | ...
    
  2. Define how to print errors in a user-friendly way:

    instance Show ProgramError where
      show (InputFileNotFound path e) = printf "File '%s' could not be read: %s" path (show e)
      ...
    
  3. Declare the type as an exception:

    instance Exception ProgramError
    
  4. Throw these exceptions in the program whenever I feel like it.

我应该捕获异常吗?

你预计会出现的异常必须被捕获和封装(例如在 InputFileNotFound 中),以便给它们更多的上下文。那么对于你没有预料到的异常呢?

我可以看到在向用户打印“这是一个错误”时有一些价值,这样他们就会将问题反馈给你。如果你这样做,你应该预料到 UserInterrupt —— 就像你所说的那样,这不是一个错误。如何处理 ThreadKilled 取决于你的应用程序 —— 实际上,取决于你是否预料到它!

然而,这与“良好的设计”无关,更取决于你针对哪种用户,你对他们的期望以及他们对你的程序的期望。

响应的范围可能从仅打印异常到显示一个对话框,上面写着“我们非常抱歉,您想向开发人员提交报告吗?”。


那么你会主张为“预期”或“已处理”异常设置指定类型吗? - MathematicalOrchid
1
听起来不错。(如果方便的话,您可以有几种这样的类型。) - Roman Cheplyaka

5

这种方式是否应该使用异常来进行流程控制?

是的。我强烈建议您阅读从循环中跳出,其中展示了EitherEitherT在其核心上只是用于提前退出代码块的抽象。异常只是这种行为的一种特殊情况,在此情况下,您会因为错误而退出,但没有理由认为这应该是您提前退出的唯一情况。


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