22

我想知道我的方法的签名应该是什么,以便我优雅地处理不同类型的失败。

这个问题是我已经对 Scala 中的错误处理提出的许多问题的总结。你可以在这里找到一些问题:


目前,我了解以下内容:

  • 两者都可以用作可能失败的方法调用的结果包装器
  • Try 是偏右的 Either,失败是非致命的异常
  • IO (scalaz) 有助于构建处理 IO 操作的纯方法
  • 所有 3 个都可以在理解中轻松使用
  • 由于不兼容的 flatMap 方法,所有 3 个都不容易在 for 理解中混合
  • 在函数式语言中,我们通常不会抛出异常,除非它们是致命的
  • 我们应该在非常特殊的情况下抛出异常。我想这是 Try 的方法
  • 创建 Throwables 对 JVM 有性能成本,并且不打算用于业务流控制

存储层

现在请考虑我有一个UserRepository. UserRepository存储用户并定义findById方法。可能会发生以下故障:

  • 致命的失败(OutOfMemoryError )
  • IO 失败,因为数据库不可访问/不可读取

此外,用户可能会丢失,从而导致Option[User]结果

使用存储库、SQL 的 JDBC 实现,可以抛出非致命异常(违反约束或其他),因此使用 Try 是有意义的。

当我们处理 IO 操作时,如果我们想要纯函数,那么 IO monad 也是有意义的。

所以结果类型可能是:

  • Try[Option[User]]
  • IO[Option[User]]
  • 别的东西?

服务层

现在让我们介绍一个业务层,UserService它提供了一些updateUserName(id,newUserName)使用之前定义的方法findById的存储库的方法。

可能会发生以下故障:

  • 所有存储库故障都传播到服务层
  • 业务错误:无法更新不存在的用户的用户名
  • 业务错误:新用户名太短

那么结果类型可能是:

  • Try[Either[BusinessError,User]]
  • IO[Either[BusinessError,User]]
  • 别的东西?

这里的 BusinessError 不是 Throwable,因为它不是异常失败。


使用理解

我想继续使用 for-comprehensions 来组合方法调用。

我们不能轻易地在理解上混合不同的 monad,所以我想我应该为我的所有操作使用某种统一的返回类型,对吧?

我只是想知道,在现实世界的 Scala 应用程序中,当可能发生不同类型的故障时,您如何成功地继续使用 for-comprehensions。

现在,为了理解对我来说效果很好,使用所有返回Either[Error,Result]但所有不同类型的故障都融合在一起的服务和存储库,处理这些故障变得有点笨拙。

您是否定义了不同类型的 monad 之间的隐式转换以便能够用于理解?

您是否定义了自己的 monad 来处理故障?

顺便说一句,也许我很快就会使用异步 IO 驱动程序。所以我想我的返回类型可能更复杂:IO[Future[Either[BusinessError,User]]]


任何建议都会受到欢迎,因为我真的不知道该使用什么,而我的应用程序并不花哨:它只是一个 API,我应该能够区分可以显示给客户端的业务错误,以及技术错误。我试图找到一个优雅而纯粹的解决方案。

4

2 回答 2

17

这就是 Scalaz 的EitherTmonad 转换器的用途。A stack ofIO[Either[E, A]]等价于EitherT[IO, E, A],除了前者必须按顺序作为多个 monad 处理,而后者自动是单个 monad,它Either为基本 monad 添加功能IO。您同样可以使用EitherT[Future, E, A]将非异常错误处理添加到异步操作。

Monad 转换器通常是在单一for理解和/或 monadic 操作中混合多个 monad 的需要的答案。


编辑:

我假设您使用的是 Scalaz 版本 7.0.0。

为了在EitherTmonad 之上使用 monad 转换IO器,首先需要导入 Scalaz 的相关部分:

import scalaz._, scalaz.effect._

您还需要定义错误类型: RepositoryErrorBusinessError等。这照常工作。您只需要确保您可以,例如,将 any 转换RepositoryError为 aBusinessError然后进行模式匹配以恢复错误的确切类型。

然后你的方法的签名变成:

def findById(id: ID): EitherT[IO, RepositoryError, User]
def updateUserName(id: ID, newUserName: String): EitherT[IO, BusinessError, User]

在您的每个方法中,您可以将基于 -and-的 monad 堆栈用作单个统一的 monad,像往常一样在 -comprehensions 中 EitherT可用。将负责在整个计算中处理基本 monad(在这种情况下),同时也以通常的方式处理错误(除非默认情况下已经右偏,因此您不必经常处理所有常见的垃圾) . 当你想做一个操作时,你所要做的就是通过使用on 的实例方法将它提升到组合的 monad 堆栈中。IOforEitherTIOEither.rightIOliftIOIO

附带说明一下,以这种方式工作时,EitherT伴生对象中的函数可能非常有用。

于 2013-05-01T19:16:22.583 回答
10

更新 2 [2020-09]:自从该答案首次​​被编辑以来,scala 生态系统发生了一些演变。cat-effect 3正在谈论有一个专用的错误通道 [2021-03 更新:它最终选择不这样做], scalaz 8停滞不前,并且从中出现了一个新库:ZIO,一个以双函子 IO monad 为核心的库( + 一个依赖注入系统,超出了当前问题的范围),在 scala 中越来越受欢迎。

因为它有专门的错误通道,刚打到v1.0.0,话题还很新颖,我回答了一个问题(和这个有关):什么是ZIO错误通道,如何感受一下里面放什么?.

它还处理更一般的问题(例如:发现应用程序的故障模式并处理它们以让未来的开发/管理系统/用户在行为上具有代理权,即使在出现错误的情况下),并简要总结了我的演讲系统错误管理应用程序. 希望它有所帮助并为该(大而复杂的)主题提供更多背景信息。


@pthariens-flame 的答案很棒,您应该将它用于手头的任务。

我想介绍该领域最近发展的一些背景背景,所以这只是一个一般信息答案。

错误管理基本上是我们的第一件工作,开发人员。快乐的道路是快乐而无聊的,不是用户抱怨的地方。大多数(全部?)问题在于过程中隐含的影响(特别是 I/O)。

管理这些问题的方法之一是遵循通常所说的“纯 FP 方法”,在程序的纯/全部和不纯/非全部部分之间画一条大红线。这样做时,您可以根据错误的种类来干净地处理错误。

最近(18 个月?),Scala在该领域进行了大量研究和开发。实际上,我相信 Scala 在这个非常具体的问题上是当今所有语言中最令人兴奋和最具颠覆性的地方(但当然,这可能只是可用性/最近信息的大脑偏见)。

Scalaz8、Monix 和猫效果是快速发展的 3 个主要贡献者。因此,与这 3 个项目(会议演讲、博客文章等)相关的任何内容都将帮助您了解正在发生的事情。

因此,简而言之,Scalaz8 将改变 IO 模型的方式,以更好地考虑错误管理。John DeGoes 在这里领导了这项工作,他在这个主题上提供了一些很好的资源:

文章:

视频:

Monix 和 Cats-effect也发生了很多事情,但我相信这个主题的大部分资源都发生在相应项目的拉取请求中。

Alexandru Nedelcu 的演讲给出了一些问题的背景:

Adam Warski 在此进行了比较:

最后,Luka Jacobowitz 为 Cats 部分撰写了一篇出色的文章:“Rethinking MonadError” https://typelevel.org/blog/2018/04/13/rethinking-monaderror.html涵盖了很多相同的领域其他光。

[编辑]:正如同行所注意到的,领域中(r)进化的跨度并没有停止在 scala-land 中。为了使效果编码(IO 等)更高效,我们做了很多工作。该领域的最新步骤是尝试使用 Kleisli Arrows 代替 monad,以最大限度地减少 JVM 上的 GC 流失。

看:

希望能帮助到你!

更新 [2018-07]:reddit 上有一个很长很有趣的话题:“有人可以向我解释 IO 的好处吗?” https://www.reddit.com/r/scala/comments/8ygjcq/can_someone_explain_to_me_the_benefits_of_io/

John DeGoes 的贡献:“Scala Wars: FP-OOP vs FP” http://degoes.net/articles/fpoop-vs-fp

于 2018-06-06T09:26:47.127 回答