【发布时间】:2020-09-03 05:05:26
【问题描述】:
在大型 Haskell 应用程序中,跨多个函数层聚合和处理类型错误是否存在一致的最佳实践?
从介绍性文本和Haskell Wiki 来看,我认为纯函数应该是完全的——也就是说,将错误作为它们共同域的一部分进行评估。运行时异常无法完全避免,但应仅限于 IO 和异步计算。
如何在纯同步函数中构建错误处理?标准建议是使用 Either 作为返回类型,然后为函数可能导致的错误定义代数数据类型 (ADT)。例如:
data OrderError
= NoLineItems
| DeliveryInPast
| DeliveryMethodUnavailable
mkOrder :: OrderDate -> Customer -> [lineIntem] -> DeliveryInfo -> Either OrderError Order
但是,一旦我尝试将多个产生错误的函数组合在一起,每个函数都有自己的错误类型,我该如何组合错误类型?我想将所有错误汇总到应用程序的 UI 层,在那里错误被解释,可能映射到特定于语言环境的错误消息,然后以统一的方式呈现给用户。当然,这个错误呈现应该不会干扰到应用的域环中的功能,应该是纯业务逻辑。
我不想定义一个 uber-type - 一个包含应用程序中所有可能错误的大型 ADT;因为这意味着 (a) 所有域级代码都需要依赖于这种类型,这会破坏所有模块化,并且 (b) 这将创建对于任何给定函数来说都太大的错误类型。
或者,我可以在每个组合函数中定义一个新的错误类型,然后将单个错误映射到组合的错误类型:比如 funA 带有错误 ADT ErrorA,funB 带有 ErrorB .如果funC,错误类型为ErrorC,同时应用funA 和funB,funC 需要将来自ErrorA 和ErrorB 的所有错误案例映射到所有属于@987654339 的新案例@。这似乎是很多样板。
第三种选择可能是funC 包装来自funA 和funB 的错误:
data ErrorC
= SomeErrorOfFunC
| ErrorsFromFunB ErrorB
| ErrorsFromFunA ErrorA
通过这种方式,映射变得更容易,但 UI 环中的错误处理需要了解应用程序内环中函数的确切嵌套。如果我重构域环,我确实需要在 UI 中触摸错误展开功能。
我确实找到了类似的question,但使用Control.Monad.Exception 的答案似乎暗示了运行时异常而不是错误返回类型。该问题的详细处理方法似乎是由 Matt Parson 撰写的this one。然而,该解决方案涉及几个 GHC 扩展、类型级编程和镜头,对于像我这样的新手来说,这需要消化很多东西,他们只是想使用 Haskell 编写一个具有适当“按书本”错误处理的体面的应用程序表现力类型系统。
我听说 PureScript 的可扩展记录可以更轻松地组合错误枚举。但是在 Haskell 中呢?有直接的最佳实践吗?如果是这样,我在哪里可以找到有关如何操作的文档或教程?
【问题讨论】:
-
为什么不反转?使用已经完成语言环境转换的单一错误类型。忘记 ADT。无论如何,您的设计必须在某个地方有一个包含所有错误的大表。您失去了“捕获”Java 风格的任意特定异常的能力,但我认为这很好。我发现“出现问题”错误和“正常操作”错误之间是有区别的,你可以给后者一个更特权的表示,这样就有类型压力来处理它们。
-
我的主要担心是域代码不需要依赖在某些外部层中声明的类型,因为这会破坏干净代码依赖规则。问题不在于某处的大型错误表,而是声明了一个错误类型,然后所有代码都需要将其作为依赖项导入。
-
经过一番阅读,在我看来,目前 GHC 中标准的可扩展异常机制(参见 original paper)也应该适用于子类型错误。
标签: haskell exception error-handling