繁体   English   中英

Haskell 错误处理方法

[英]Haskell approaches to error handling

这里没有争论 Haskell 中有多种机制来处理错误并正确处理它们。 Error monad、Either、Maybe、异常等。

那么为什么用其他语言编写易发生异常的代码比用 Haskell 更直接?

假设我想编写一个命令行工具来处理在命令行上传递的文件。 我想:

  • 验证提供了文件名
  • 验证文件是否可用且可读
  • 验证文件具有有效的标题
  • 创建 output 文件夹并验证 output 文件是否可写
  • 处理文件、解析错误、不变错误等。
  • Output 文件,写入错误,磁盘已满等错误。

所以一个非常直接的文件处理工具。

在 Haskell 中,我会将此代码包装在某种单子组合中,使用 Maybe 和 Either,并根据需要翻译和传播错误。 最后,这一切都得到了一个 IO monad,我可以在其中向用户提供 output 的状态。

在另一种语言中,我只是抛出一个异常并在适当的地方捕获。 直截了当。 我不会花太多时间在认知边缘试图解开我需要什么样的机制组合。

我只是在接近这个错误还是这种感觉有一些实质内容?

编辑:好的,我收到反馈,告诉我感觉更难,但实际上并非如此。 所以这是一个痛点。 在 Haskell 中,我正在处理单子堆栈,如果我必须处理错误,我将在此单子堆栈中添加另一层。 我不知道我必须添加多少电梯和其他语法垃圾才能使代码编译但添加零语义意义。 没有人觉得这增加了复杂性?

在 Haskell 中,我会将此代码包装在某种单子组合中,使用 Maybe 和 Either,并根据需要翻译和传播错误。 最后,这一切都得到了一个 IO monad,我可以在其中向用户提供 output 的状态。

在另一种语言中,我只是抛出一个异常并在适当的地方捕获。 直截了当。 我不会花太多时间在认知边缘试图解开我需要什么样的机制组合。

我不会说你一定是错误地接近它。 相反,您的错误在于认为这两种情况是不同的。 他们不是。

“简单地抛出和捕获”相当于将与 Haskell 错误处理方法的某种组合完全相同的概念结构强加于您的整个程序。 确切的组合取决于您要与之比较的语言的错误处理系统,这说明了 Haskell看起来更复杂的原因:它允许您根据需要混合和匹配错误处理结构,而不是给您一个隐含的、一个- 最适合的解决方案。

所以,如果你需要一种特殊的错误处理方式,你可以使用它; 并且您仅将它用于需要它的代码。 不需要它的代码——由于既不生成也不处理相关类型的错误——被标记为这样,这意味着您可以使用该代码而不必担心会产生这种错误。


关于句法笨拙的主题,这是一个尴尬的主题。 理论上,它应该是无痛的,但是:

  • Haskell 一直是一种研究驱动的语言,在其早期,许多东西仍在不断变化,有用的习语还没有普及,所以旧代码很可能是一个糟糕的角色 model
  • 一些库在处理错误方面没有那么灵活,要么是由于上述旧代码的僵化,要么只是缺乏修饰
  • 我不知道有关如何最好地构造新代码以进行错误处理的任何指南,因此新手只能自己动手

我猜你可能以某种方式“做错了”,并且可以避免大部分语法混乱,但期望你(或任何普通的 Haskell 程序员)自己找到最好的方法可能是不合理的.

至于 monad 转换器堆栈newtype ,我认为标准方法是为您的应用程序新建整个堆栈,派生或实现相关类型类的实例(例如MonadError ),然后使用通常不需要的类型类的功能lift 您为应用程序的核心编写的一元函数都应该使用newtype d 堆栈,因此也不需要提升。 我认为,唯一无法避免的低语义含义是liftIO

处理大量的转换器可能是一个真正令人头疼的问题,但只有当有很多不同转换器的嵌套层时(堆积StateTErrorT的交替层,中间有一个ContT ,然后试着告诉我你的代码是什么实际上会这样做)。 不过,这很少是您真正想要的。


编辑:作为一个小附录,我想提请注意我在写一些评论时发生的更普遍的观点。

正如我所说并且@sclv 很好地展示了,正确的错误处理确实那么复杂。 您所能做的就是改变这种复杂性,而不是消除它,因为无论您执行什么可能会独立产生错误的多个操作,您的程序都需要以某种方式处理所有可能的组合,即使这种“处理”只是简单地失败结束并死去。

也就是说,Haskell 在一个方面确实与大多数语言有本质上的不同:通常,错误处理是明确的和一流的,这意味着一切都是公开的并且可以自由操作。 这样做的另一面是隐式错误处理的丢失,这意味着即使您只想打印错误消息并死掉,您也必须明确地这样做。 所以实际上在 Haskell 中进行错误处理更容易,因为它具有一流的抽象,但忽略错误更难。 然而,在任何现实世界的生产使用中,那种“所有人都弃船”错误几乎永远不会正确,这就是为什么看起来尴尬被抛在一边的原因。

因此,当您需要明确地处理错误时,虽然一开始事情确实会更复杂,但重要的是要记住这就是它的全部内容 一旦你学会了如何使用正确的错误处理抽象,复杂性几乎达到了一个平台,并且随着程序的扩展并没有真正变得更加困难。 你使用这些抽象的次数越多,它们就越自然。

让我们看看您想要做的一些事情:

验证提供了文件名

如果他们不是? 干脆放弃吧?

验证文件是否可用且可读

如果有些不是? 处理剩下的,当你遇到坏的时抛出异常,警告坏的并处理好的? 在做任何事情之前退出?

验证文件具有有效的标题

如果他们不这样做? 同样的问题——跳过坏的,提前中止,警告坏的,等等......

处理文件、解析错误、不变错误等。

再次,做什么,跳过坏行,跳过坏文件,中止,中止和回滚,打印警告,打印可配置的警告级别?

关键是有可用的选择和选项。 要以一种反映命令式的方式做你想做的事,你根本不需要任何可能或任何一个单子堆栈。 您所需要的只是在 IO 中抛出和捕获异常。

如果你不想到处使用异常,并获得一定程度的控制,你仍然可以在没有 monad 堆栈的情况下做到这一点。 例如,如果您想处理可以处理的文件并获得结果,并在无法处理的文件上返回错误,那么 Eithers 工作得很好——只需编写 function of FilePath -> IO (Either String Result) 然后将其mapM到您的输入文件列表上。 然后partitionEithers结果列表,然后 mapM 一个 function 的Result -> IO (Maybe String)覆盖结果,并catMaybe错误字符串。 现在您可以mapM print <$> (inputErrors ++ outputErrors)来显示两个阶段中出现的所有错误。

或者,你知道,你也可以做其他事情。 在任何情况下,在 monad 堆栈中使用MaybeEither都有它的位置。 但是对于典型的错误处理案例来说,直接显式的处理更方便,功能也很强大。 只需要一些时间来适应各种各样的功能,就可以方便地操作它们。

评估到Either ea和模式匹配与trycatch有什么区别,除了它传播异常的事实(如果你使用 Either monad,你可以模拟这个)

请记住,大多数情况下,单子使用某些东西(在我看来)是丑陋的,除非您大量使用可能会失败的函数。

如果你只有一个可能的失败,那就没有错

func x = case tryEval x of
             Left e -> Left e
             Right val -> Right $ val + 1

func x = (+1) <$> trvEval x

它只是表示同一事物的一种功能方式。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM