如何在 Haskell 中编写错误类型?

Ulr*_*ter 8 error-handling haskell exception

在更大的 Haskell 应用程序中,是否有一个一致的最佳实践来聚合和处理跨多个函数层的类型错误?

来自介绍性文本和Haskell Wiki,我认为纯函数应该是完整的——也就是说,将错误评估为它们的共同域的一部分。运行时异常无法完全避免,而应仅限于 IO 和异步计算。

如何在纯同步函数中构建错误处理?标准建议是使用Either作为返回类型,然后为函数可能导致的错误定义代数数据类型 (ADT)。例如:

data OrderError
    = NoLineItems
    | DeliveryInPast
    | DeliveryMethodUnavailable

mkOrder :: OrderDate -> Customer -> [lineIntem] -> DeliveryInfo -> Either OrderError Order
Run Code Online (Sandbox Code Playgroud)

但是,一旦我尝试将多个产生错误的函数组合在一起,每个函数都有自己的错误类型,我该如何组合错误类型?我想将所有错误聚合到应用程序的 UI 层,在那里解释错误,可能映射到特定于语言环境的错误消息,然后以统一的方式呈现给用户。当然,这种错误呈现不应该干扰应用程序域环中的功能,应该是纯业务逻辑。

我不想定义一个超级类型——一个包含应用程序中所有可能错误的大型 ADT;因为这意味着 (a) 所有域级代码都需要依赖于这种类型,这会破坏所有的模块化,以及 (b) 这将创建对于任何给定函数来说都太大的错误类型。

或者,我可以在每个组合函数中定义一个新的错误类型,然后将各个错误映射到组合错误类型:说funA带有 error-ADT ErrorA,并funB带有ErrorB. 如果funC,有错误类型ErrorC,均适用funAfunBfunC需要所有错误的情况下,从地图ErrorAErrorB新的案件是所有部分ErrorC。这似乎是很多样板。

第三个选项可能是funC包装来自funA和的错误funB

data ErrorC
    = SomeErrorOfFunC
    | ErrorsFromFunB ErrorB
    | ErrorsFromFunA ErrorA
Run Code Online (Sandbox Code Playgroud)

通过这种方式,映射变得更容易,但 UI 环中的错误处理需要知道应用程序内环中函数的确切嵌套。如果我重构域环,我确实需要触摸 UI 中的错误解包功能。

我确实找到了一个类似的问题,但是使用Control.Monad.Exception的答案似乎暗示了运行时异常而不是错误返回类型。对这个问题的详细处理似乎是马特·帕森 (Matt Parson) 的这篇文章。然而,该解决方案涉及几个 GHC 扩展、类型级编程和镜头,对于像我这样的新手来说,需要消化很多东西,他们只是想使用 Haskell 的正确“按书”错误处理来编写一个像样的应用程序表达类型系统。

我听说 PureScript 的可扩展记录可以更轻松地组合错误枚举。但是在 Haskell 中?有没有直接的最佳实践?如果是这样,我在哪里可以找到有关如何操作的文档或教程?

sha*_*yan 1

对于您的可聚合Error类型,我建议您查找验证:类似于 Either 的数据类型,但具有累积的 Applicative

该库就是一个模块,仅包含少量定义。Validation包内的类型本质上是(尽管不是字面上的):

type Validation e a = Either (NonEmpty e) a
Run Code Online (Sandbox Code Playgroud)

值得指出的是,误差的累积是使用应用组合器(即liftA2liftA3和 )来实现的zip。您不能monad在推导式(又名表示法)中累积错误do

user :: Int -> Validation DomainError User
userId :: User -> Int
post :: Int -> Validation DomainError Post

userAndPost = do 
  u <- user 1
  p <- post . userId $ u
  return $ (u,p)
Run Code Online (Sandbox Code Playgroud)

另一方面,应用版本可能会产生两个错误:

userAndPostA2 = liftA2 (,) (user 1) (post 1)    
Run Code Online (Sandbox Code Playgroud)

上面函数的 monad 版本userAndPost永远不会产生两个错误:既没有找到user,也post没有找到。总是一个或另一个。应用程序虽然在理论上被认为不如 monad 强大,但在某些实践中具有独特的优势。应用程序相对于 monad 的另一个优势是并发性。再次使用上面的例子,我们可以很容易地推断出为什么推导式中的 monad永远不能同时执行(注意帖子的获取如何取决于所获取用户的用户 ID,从而表明一个操作的执行取决于另一个)。

至于您在选择DomainError为所有域级别错误定义单个不相交的联合类型时担心破坏代码模块化,我敢说没有更好的方法对其进行建模,只要仅构造所述特定于域的错误类型并由领域层中的函数传递。一旦 HTTP 层调用了域层的函数,它就需要将域层的错误转换为它自己的错误,例如通过类似于以下的映射函数:

eDomainToHTTP :: DomainError -> HTTPError
eDomainToHTTP InvalidCredentials = Forbidden403
eDomainToHTTP UserNotFound = NotFound404
eDomainToHTTP PostNotFound = NotFound404
Run Code Online (Sandbox Code Playgroud)

通过这样一个函数,您可以轻松地将任何转换input -> Validation DomainError outputinput -> Validation HTTPError output​​ ,从而保留代码库中的封装性和模块化性。