简体   繁体   English

Haskell 错误处理方法

[英]Haskell approaches to error handling

No argument here that there are a variety of mechanisms in place in Haskell to handle errors and properly handle them.这里没有争论 Haskell 中有多种机制来处理错误并正确处理它们。 Error monad, Either, Maybe, exceptions, etc. Error monad、Either、Maybe、异常等。

So why is it that it feels much more straightforward writing exception-prone code in other languages than in Haskell?那么为什么用其他语言编写易发生异常的代码比用 Haskell 更直接?

Let's say I'd like to write a command line tool that processes files passed on the command line.假设我想编写一个命令行工具来处理在命令行上传递的文件。 I'd like to:我想:

  • Verify filenames are provided验证提供了文件名
  • Verify files are available and readable验证文件是否可用且可读
  • Verify files have valid headers验证文件具有有效的标题
  • Create output folder and verify output files will be writable创建 output 文件夹并验证 output 文件是否可写
  • Process files, erroring on parsing errors, invariant errors, etc.处理文件、解析错误、不变错误等。
  • Output files, erroring on write error, disk full, etc. Output 文件,写入错误,磁盘已满等错误。

So a pretty straight file processing tool.所以一个非常直接的文件处理工具。

In Haskell, I'd be wrapping this code in some combination of monads, using Maybe's and Either's and translating and propagating errors as necessary.在 Haskell 中,我会将此代码包装在某种单子组合中,使用 Maybe 和 Either,并根据需要翻译和传播错误。 In the end, it all gets to an IO monad where I am able to output the status to the user.最后,这一切都得到了一个 IO monad,我可以在其中向用户提供 output 的状态。

In another language, I simply throw an exception and catch in the appropriate place.在另一种语言中,我只是抛出一个异常并在适当的地方捕获。 Straightforward.直截了当。 I don't spend much time in cognitive limbo trying to unravel what combination of mechanisms I need.我不会花太多时间在认知边缘试图解开我需要什么样的机制组合。

Am I simply approaching this wrong or is this there some substance to this feeling?我只是在接近这个错误还是这种感觉有一些实质内容?

Edit: Okay, I'm getting feedback telling me that it just feels harder but actually isn't.编辑:好的,我收到反馈,告诉我感觉更难,但实际上并非如此。 So here is one pain point.所以这是一个痛点。 In Haskell, I'm dealing with stacks of monads, and if I have to handle errors, I'm adding another layer to this monad stack.在 Haskell 中,我正在处理单子堆栈,如果我必须处理错误,我将在此单子堆栈中添加另一层。 I don't know how many lift's and and other syntactic litter I've had to add just to make the code compile but adds zero semantic meaning.我不知道我必须添加多少电梯和其他语法垃圾才能使代码编译但添加零语义意义。 No one feels this adds to the complexity?没有人觉得这增加了复杂性?

In Haskell, I'd be wrapping this code in some combination of monads, using Maybe's and Either's and translating and propagating errors as necessary.在 Haskell 中,我会将此代码包装在某种单子组合中,使用 Maybe 和 Either,并根据需要翻译和传播错误。 In the end, it all gets to an IO monad where I am able to output the status to the user.最后,这一切都得到了一个 IO monad,我可以在其中向用户提供 output 的状态。

In another language, I simply throw an exception and catch in the appropriate place.在另一种语言中,我只是抛出一个异常并在适当的地方捕获。 Straightforward.直截了当。 I don't spend much time in cognitive limbo trying to unravel what combination of mechanisms I need.我不会花太多时间在认知边缘试图解开我需要什么样的机制组合。

I wouldn't say you're necessarily approaching it wrong.我不会说你一定是错误地接近它。 Rather, your mistake is in thinking that these two scenarios are different;相反,您的错误在于认为这两种情况是不同的。 they're not.他们不是。

To "simply throw and catch" is equivalent to imposing upon your entire program the exact same conceptual structure as some combination of Haskell's error-handling methods. “简单地抛出和捕获”相当于将与 Haskell 错误处理方法的某种组合完全相同的概念结构强加于您的整个程序。 The exact combination depends on the error-handling systems of the language you're comparing it to, which points to why Haskell seems more complicated: It lets you mix and match error handling structures based on need, rather than giving you an implicit, one-size-fits-most solution.确切的组合取决于您要与之比较的语言的错误处理系统,这说明了 Haskell看起来更复杂的原因:它允许您根据需要混合和匹配错误处理结构,而不是给您一个隐含的、一个- 最适合的解决方案。

So, if you need a particular style of error handling, you use it;所以,如果你需要一种特殊的错误处理方式,你可以使用它; and you use it for only the code that needs it.并且您仅将它用于需要它的代码。 Code that doesn't need it--due to neither generating nor handling the relevant sorts of errors--is marked as such, meaning you can use that code without worrying about that sort of error being created.不需要它的代码——由于既不生成也不处理相关类型的错误——被标记为这样,这意味着您可以使用该代码而不必担心会产生这种错误。


On the subject of syntactic clumsiness, that's an awkward subject.关于句法笨拙的主题,这是一个尴尬的主题。 In theory, it should be painless, but:理论上,它应该是无痛的,但是:

  • Haskell has been a research-driven language for a while, and in its early days many things were still in flux and useful idioms hadn't been popularized yet, so old code floating around is likely to be a poor role model Haskell 一直是一种研究驱动的语言,在其早期,许多东西仍在不断变化,有用的习语还没有普及,所以旧代码很可能是一个糟糕的角色 model
  • Some libraries are not as flexible as they could be in how errors are handled, either due to fossilization of old code as above, or just lack of polish一些库在处理错误方面没有那么灵活,要么是由于上述旧代码的僵化,要么只是缺乏修饰
  • I'm not aware of any guides on how to best structure new code for error handling, so newcomers are left to their own devices我不知道有关如何最好地构造新代码以进行错误处理的任何指南,因此新手只能自己动手

I'd guess that chances are you're "doing it wrong" somehow, and could avoid most of that syntactic clutter, but that it's probably not reasonable to expect you (or any average Haskell programmer) to find the best approaches on their own.我猜你可能以某种方式“做错了”,并且可以避免大部分语法混乱,但期望你(或任何普通的 Haskell 程序员)自己找到最好的方法可能是不合理的.

As far as monad transformer stacks go, I think the standard approach is to newtype the entire stack for your application, derive or implement instances for the relevant type classes (eg, MonadError ), then use the type class's functions which won't generally need lift ing.至于 monad 转换器堆栈newtype ,我认为标准方法是为您的应用程序新建整个堆栈,派生或实现相关类型类的实例(例如MonadError ),然后使用通常不需要的类型类的功能lift Monadic functions you write for the core of your application should all use the newtype d stack, so won't need lifting, either.您为应用程序的核心编写的一元函数都应该使用newtype d 堆栈,因此也不需要提升。 About the only low-semantic-meaning thing you can't avoid is liftIO , I think.我认为,唯一无法避免的低语义含义是liftIO

Dealing with large stacks of transformers can be an actual headache, but only when there's a lot of nested layers of different transformers (pile up alternating layers of StateT and ErrorT with a ContT tossed in the middle, then just try to tell me what your code will actually do).处理大量的转换器可能是一个真正令人头疼的问题,但只有当有很多不同转换器的嵌套层时(堆积StateTErrorT的交替层,中间有一个ContT ,然后试着告诉我你的代码是什么实际上会这样做)。 This is rarely what you actually want, though.不过,这很少是您真正想要的。


Edit : As a minor addendum, I want to bring attention to more general point that occurred to me while writing a couple comments.编辑:作为一个小附录,我想提请注意我在写一些评论时发生的更普遍的观点。

As I remarked and @sclv demonstrated nicely, correct error-handling really is that complicated.正如我所说并且@sclv 很好地展示了,正确的错误处理确实那么复杂。 All you can do is shuffle that complexity around, not eliminate it, because no matter what you're performing multiple operations that can produce errors independently and your program needs to handle every possible combination somehow, even if that "handling" is to simply fall over and die.您所能做的就是改变这种复杂性,而不是消除它,因为无论您执行什么可能会独立产生错误的多个操作,您的程序都需要以某种方式处理所有可能的组合,即使这种“处理”只是简单地失败结束并死去。

That said, Haskell really does differ intrinsically from most languages in one regard: Generally, error-handling is both explicit and first-class , meaning that everything is out in the open and can be manipulated freely.也就是说,Haskell 在一个方面确实与大多数语言有本质上的不同:通常,错误处理是明确的和一流的,这意味着一切都是公开的并且可以自由操作。 The flip side of this is a loss of implicit error-handling, meaning that even if all you want is to print an error message and die, you have to do so explicitly.这样做的另一面是隐式错误处理的丢失,这意味着即使您只想打印错误消息并死掉,您也必须明确地这样做。 So actually doing error-handling is easier in Haskell, because of first-class abstractions for it, but ignoring errors is harder.所以实际上在 Haskell 中进行错误处理更容易,因为它具有一流的抽象,但忽略错误更难。 However, that sort of "all hands abandon ship" error non-handling is almost never correct in any sort of real-world, production use, which is why it seems like awkwardness gets brushed aside.然而,在任何现实世界的生产使用中,那种“所有人都弃船”错误几乎永远不会正确,这就是为什么看起来尴尬被抛在一边的原因。

So, while it's true that things are more complicated at first when you need to deal with errors explicitly, the important thing is to remember that that's all there is to it .因此,当您需要明确地处理错误时,虽然一开始事情确实会更复杂,但重要的是要记住这就是它的全部内容 Once you learn how to use the proper error-handling abstractions, the complexity pretty much hits a plateau and doesn't really get significantly harder as a program expands;一旦你学会了如何使用正确的错误处理抽象,复杂性几乎达到了一个平台,并且随着程序的扩展并没有真正变得更加困难。 and the more you use those abstractions the more natural they become.你使用这些抽象的次数越多,它们就越自然。

Let's look at some of what you want to do:让我们看看您想要做的一些事情:

Verify filenames are provided验证提供了文件名

And if they aren't?如果他们不是? Just quit, right?干脆放弃吧?

Verify files are available and readable验证文件是否可用且可读

And if some aren't?如果有些不是? Process the remaining ones, throw an exception when you hit a bad one, warn on the bad ones and handle the good ones?处理剩下的,当你遇到坏的时抛出异常,警告坏的并处理好的? Quit before doing anything?在做任何事情之前退出?

Verify files have valid headers验证文件具有有效的标题

And if they don't?如果他们不这样做? Same issue -- skip the bad ones, abort early, warn on the bad ones, etc...同样的问题——跳过坏的,提前中止,警告坏的,等等......

Process files, erroring on parsing errors, invariant errors, etc.处理文件、解析错误、不变错误等。

Again, and do what, skip the bad lines, skip the bad files, abort, abort and rollback, print warnings, print configurable levels of warnings?再次,做什么,跳过坏行,跳过坏文件,中止,中止和回滚,打印警告,打印可配置的警告级别?

The point is that there are choices and options available.关键是有可用的选择和选项。 To do what you want in a way that mirrors the imperative way, you don't need any maybes or eithers of monad stacks at all.要以一种反映命令式的方式做你想做的事,你根本不需要任何可能或任何一个单子堆栈。 All you need is throwing and catching exceptions in IO.您所需要的只是在 IO 中抛出和捕获异常。

And if you want to not use exceptions all over, and get a degree of control, you can still do it without monad stacks.如果你不想到处使用异常,并获得一定程度的控制,你仍然可以在没有 monad 堆栈的情况下做到这一点。 For example, if you want to process the files you can and get results, and return errors on the files you can't, then Eithers work great -- just write a function of FilePath -> IO (Either String Result) .例如,如果您想处理可以处理的文件并获得结果,并在无法处理的文件上返回错误,那么 Eithers 工作得很好——只需编写 function of FilePath -> IO (Either String Result) Then mapM that over your list of input files.然后将其mapM到您的输入文件列表上。 Then partitionEithers the resultant list, and then mapM a function of Result -> IO (Maybe String) over the results, and catMaybe the error strings.然后partitionEithers结果列表,然后 mapM 一个 function 的Result -> IO (Maybe String)覆盖结果,并catMaybe错误字符串。 Now you can mapM print <$> (inputErrors ++ outputErrors) to display all the errors that came up in both phases.现在您可以mapM print <$> (inputErrors ++ outputErrors)来显示两个阶段中出现的所有错误。

Or, you know, you can do something else too.或者,你知道,你也可以做其他事情。 In any case, using Maybe and Either in monad stacks has its place.在任何情况下,在 monad 堆栈中使用MaybeEither都有它的位置。 But for the typical error handling cases, its more convenient to deal with them directly and explicitly, and very powerful too.但是对于典型的错误处理案例来说,直接显式的处理更方便,功能也很强大。 It just takes some getting used to the large variety of functions to make their manipulation convenient.只需要一些时间来适应各种各样的功能,就可以方便地操作它们。

What is the difference between evaluating to Either ea and pattern matching, vs a try and catch , besides the fact that it propagates with exceptions (And if you use the Either monad, you can simulate this)评估到Either ea和模式匹配与trycatch有什么区别,除了它传播异常的事实(如果你使用 Either monad,你可以模拟这个)

Keep in mind that most of the time the monadic use of something is (in my opinion) ugly, unless you have a significant amount of usage of functions that can fail.请记住,大多数情况下,单子使用某些东西(在我看来)是丑陋的,除非您大量使用可能会失败的函数。

If you only have one possible failure, theres nothing wrong with如果你只有一个可能的失败,那就没有错

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

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

Its just a functional way of representing the same thing.它只是表示同一事物的一种功能方式。

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

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