Haskell处理错误处理

Mui*_*uin 40 haskell exception

这里没有争论,Haskell中有各种各样的机制来处理错误并妥善处理它们.错误monad,Either,Maybe,exception等.

那么为什么在Haskell中编写其他语言中容易出现异常的代码感觉更直接呢?

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

  • 验证是否提供了文件名
  • 验证文件可用且可读
  • 验证文件具有有效标头
  • 创建输出文件夹并验证输出文件是否可写
  • 处理文件,解析错误时出错,不变错误等.
  • 输出文件,写入错误错误,磁盘已满等.

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

在Haskell中,我将使用Maybe和Either将这些代码包装在monad的某些组合中,并根据需要翻译和传播错误.最后,它都进入IO monad,我可以将状态输出给用户.

在另一种语言中,我只是抛出异常并捕获到适当的位置.直截了当.我不会花太多时间在认知困境中试图解开我需要的机制组合.

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

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

C. *_*ann 32

在Haskell中,我将使用Maybe和Either将这些代码包装在monad的某些组合中,并根据需要翻译和传播错误.最后,它都进入IO monad,我可以将状态输出给用户.

在另一种语言中,我只是抛出异常并捕获到适当的位置.直截了当.我不会花太多时间在认知困境中试图解开我需要的机制组合.

我不会说你一定是接近错了.相反,你的错误是认为这两种情况不同; 他们不是.

"简单地抛出和捕获"等同于将整个程序强加于与Haskell的错误处理方法的某种组合完全相同的概念结构.确切的组合取决于你所比较的语言的错误处理系统,这就说明了为什么Haskell 看起来更复杂:它允许你根据需要混合和匹配错误处理结构,而不是给你一个隐含的,一个适合大多数解决方案.

因此,如果您需要特定类型的错误处理,则使用它; 并且它仅用于需要它的代码.不需要它的代码 - 由于既不生成也不处理相关类型的错误 - 被标记为这样,这意味着您可以使用该代码而不必担心创建的那种错误.


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

  • Haskell一段时间以来一直是一种研究驱动的语言,并且在早期许多事情仍然不断变化,有用的习惯用法尚未普及,因此旧的代码可能是一个糟糕的榜样
  • 有些库不像处理错误那样灵活,可能是由于上面的旧代码僵化,或者只是缺乏抛光
  • 我不知道有关如何最好地构造用于错误处理的新代码的任何指南,因此新手会留给他们自己的设备

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

就monad变换器堆栈而言,我认为标准方法是newtype针对应用程序的整个堆栈,派生或实现相关类型类的实例(例如,MonadError),然后使用通常不需要的类型类的函数lift.您为应用程序核心编写的Monadic函数应该都使用newtyped堆栈,因此也不需要提升.liftIO我想,关于唯一一个你无法避免的低语义意义的事情.

与变压器的大叠处理可以是实际的头痛,但只有当有很多不同变压器的层层嵌套(摞起来的交替层StateT,并ErrorTContT中间扔,那么就试着告诉我你的代码会怎么做).但这很少是你真正想要的.


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

正如我所说,并且@sclv很好地证明了,正确的错误处理确实复杂.所有你能做的就是改变这种复杂性,而不是消除它,因为无论你执行多项操作都可以独立产生错误,你的程序需要以某种方式处理每种可能的组合,即使这种"处理"只是简单地落下过度死亡.

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

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

  • 我希望我能为倒数第二段+2。 (2认同)

scl*_*clv 26

让我们来看看你想做的一些事情:

验证是否提供了文件名

如果他们不是?刚退出吧?

验证文件可用且可读

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

验证文件具有有效标头

如果他们不这样做?同样的问题 - 跳过坏的,早点中止,警告坏的等等......

处理文件,解析错误时出错,不变错误等.

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

关键是有可供选择和选择.要以一种反映命令式方式的方式做你想做的事,你根本不需要monad堆栈的任何maybes或eithers.您只需要在IO中抛出和捕获异常.

如果你不想全部使用异常,并获得一定程度的控制,你仍然可以在没有monad堆栈的情况下完成它.例如,如果你想处理文件并获得结果,并返回你不能的文件上的错误,那么Eithers工作得很好 - 只需编写一个函数FilePath -> IO (Either String Result).然后mapM在你的输入文件列表上.然后partitionEithers是结果列表,然后mapM一个Result -> IO (Maybe String)结果的函数,以及catMaybe错误字符串.现在,您可以mapM print <$> (inputErrors ++ outputErrors)显示两个阶段中出现的所有错误.

或者,你知道,你也可以做其他事情.在任何情况下,使用MaybeEither在monad堆栈中都有它的位置.但对于典型的错误处理案例,直接和明确地处理它们更方便,而且非常强大.它只需要一些习惯于各种各样的函数来使它们的操作变得方便.

  • 好答案.我发现自己在Java中缺少Maybe和Either,而不是在Haskell中缺少异常的语法糖. (2认同)
  • 最终,这一切都归结为处理每一个可能结果的必要性。模式匹配、“partitionEithers”、“catchError”等之间的区别仅在于聚合哪些情况、哪些函数取决于哪些其他结果以及在代码中处理不同情况的位置。没有办法避免这种复杂性;Haskell 只是不鼓励你把它隐藏起来。 (2认同)
  • @Muin你会发现两个有用的错误处理示例[这里](http://donsbot.wordpress.com/2010/08/17/practical-haskell/)和[here](http://snoyberg.wordpress. COM/2009/10/25 /介绍到尝试 - 错误报告库/). (2认同)

alt*_*ive 7

评估Either e a和模式匹配之间的区别是什么,vs a trycatch除了它与异常一起传播的事实之外(如果你使用Either monad,你可以模拟这个)

请记住,大多数情况下,monadic使用某些东西(在我看来)是丑陋的,除非你有大量使用可能失败的功能.

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

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

func x = (+1) <$> trvEval x
Run Code Online (Sandbox Code Playgroud)

它只是表示同一事物的功能性方式.

  • 编写`fmap(1+)`这是一个很长的路.事实上,你只需要添加fmap,实际上对我来说非常轻巧.也适用于Maybe monad.对于IO中的异常,同样的事情也不会起作用吗?或许"使用`fmap`"对这个问题来说是一个不错的答案. (3认同)