在Haskell中,我会使用一些单子的组合方式,使用Maybe和Either来翻译和传递错误。最终它都会到IO单子,我可以在那里向用户输出状态。
在另一种语言中,我只需抛出异常并在适当的位置捕获即可。直截了当。我不会花费太多时间在认知限制中,试图解开我需要哪些机制的组合。
我不会说你一定做错了。相反,你的错误在于认为这两种情况是不同的,而它们并不是。“简单地抛出并捕获”等同于在整个程序上强加与Haskell的错误处理方法完全相同的概念结构。确切的组合取决于您要比较的语言的错误处理系统,这就是为什么Haskell看起来更复杂的原因:它允许您根据需要混合和匹配错误处理结构,而不是给您一个隐含的、一种大小适合大多数情况的解决方案。因此,如果您需要特定的错误处理样式,就使用它;并且您只对需要它的代码使用它。不需要它的代码--由于既不生成也不处理相关类型的错误--被标记为这样,这意味着您可以使用该代码,而不必担心该类型的错误被创建。
关于语法笨拙的问题,这是一个尴尬的问题。理论上,它应该是无痛的,但是:
- Haskell已经成为一个研究驱动的语言有一段时间了,在它的早期阶段,许多事情仍然在变化中,有用的惯用语还没有被普及,因此,老代码漂浮在周围可能是一个糟糕的角色模型
- 一些库在处理错误的灵活性方面并不像它们本应该的那样灵活,要么是由于以上原因的旧代码的化石化,要么只是缺乏光彩。
- 我不知道有任何关于如何为错误处理最佳结构化新代码的指南,所以新手只能靠自己的设备。
我猜你在某些方面“做错了”,可能可以避免大部分的语法混乱,但可能不合理期望你(或任何普通的Haskell程序员)自己找到最好的方法。
至于monad变换器栈,我认为标准的方法是为应用程序newtype
整个栈,派生或实现相关类型类的实例(例如MonadError
),然后使用类型类的函数,这些函数通常不需要lift
。您为核心应用程序编写的单调函数都应使用newtype
的堆栈,因此也不需要提升。我认为,唯一无法避免的低语义含义是liftIO
。
处理大量变换器堆栈可能确实会导致头痛,但仅当存在许多嵌套的不同转换器层时才会出现这种情况(将交替层堆叠StateT
和ErrorT
,并在中间添加一个ContT
,然后尝试告诉我您的代码实际上会执行什么操作)。然而,这很少是您真正想要的。
编辑:作为一个小补充,我想提醒更一般的观点,在写几个评论时我注意到了这一点。
正如我所指出的,@sclv非常好地展示了正确的错误处理确实是如此复杂。您所能做的就是将该复杂性分散,而不是消除它,因为无论您执行哪些可以独立产生错误的操作,您的程序都需要以某种方式处理每种可能的组合,即使该“处理”仅是跌倒和死亡。
话虽这样说,但Haskell在一个方面与大多数语言 intrinsically(本质上)存在着明显的区别:一般来说,错误处理是明确且first-class(头等重要的),这意味着所有的东西都是公开的并且可以自由地进行操作。这样做的反面是隐式错误处理的丧失,这意味着即使你只想打印出一个错误信息并退出程序,你也必须显式地这样做。因此,在 Haskell 中实际进行错误处理更容易,因为有用于此的头等抽象,但忽略错误则更难。然而,这种“放弃所有”的错误非处理几乎在任何实际的生产应用中都是不正确的,这就是为什么这种笨拙的方式经常被忽略。
因此,尽管在需要显式处理错误时事情一开始可能更加复杂,但重要的是要记住那就是全部。一旦学会了如何使用适当的错误处理抽象,复杂性基本上就达到了一个平稳阶段,并且随着程序扩展,它并没有变得显著更难;而且你使用这些抽象的次数越多,它们就变得越自然。
Control.Exception
库。可以在任何地方抛出异常,在 IO monad 中捕获。它们发生了什么事情? - n. m.