【问题标题】:Try[Result], IO[Result], Either[Error,Result], which should I use in the endTry[Result], IO[Result], Either[Error,Result],到底应该用哪个
【发布时间】:2013-04-25 11:29:04
【问题描述】:

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

这个问题在某种程度上是我对 Scala 错误处理的许多问题的总结。您可以在这里找到一些问题:


目前,我了解以下内容:

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

存储库层

现在请考虑我有一个UserRepositoryUserRepository 存储用户并定义了一个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 混合在一个 for-comprehension 中,所以我想我应该为我的所有操作设置某种统一的返回类型,对吧?

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

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

您是否定义了不同类型的 monad 之间的隐式转换以便能够使用 for-comprehensions?

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

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


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

【问题讨论】:

  • 能否请您发布一些代码,说明您最终是如何编写这个的?我很感兴趣,因为我有类似的模式。我的 scalaz 技能不是我想要的……我很难理解这些课程。

标签: scala scalaz either


【解决方案1】:

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

因为有专门的报错频道,刚打到v1.0.0,题目还是很新颖,我回答了这个问题(和这个有关):What is ZIO error channel and how to get a feeling about what to put in it?

它还处理更一般的问题(例如:发现应用程序的故障模式并处理它们以让未来的 dev/adminsys/users 对行为有代理权,即使在出现错误的情况下),并简要总结给我的talk systematic error management in application .希望它对这个(大而复杂的)主题有所帮助并提供更多背景信息。


@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 churn。

见:

希望对你有帮助!

更新 [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

【讨论】:

  • 目前使用猫特效 IO 的技术水平如何?
  • AFAIK,cats-effect 3 不会改变错误管理的一般架构(另一方面,它是关于纤维的重大演变)。
  • 谢谢??为此!在 scala fp 世界中,对于错误管理的理念确实存在分歧! Zio 和猫
【解决方案2】:

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

一般来说,Monad 转换器可以满足在单个 for-comprehension 和/或 monadic 操作中混合多个 monad 的需求。


编辑:

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

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

import scalaz._, scalaz.effect._

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

那么你的方法的签名就变成了:

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

在您的每个方法中,您都可以将基于 EitherT-and-IO 的 monad 堆栈用作单个统一的 monad,像往常一样在 for-comprehensions 中提供。 EitherT 将负责在整个计算中处理基本 monad(在本例中为 IO),同时也以 Either 通常的方式处理错误(默认情况下已经右偏,所以你没有不断处理所有常见的.right垃圾)。当你想做一个IO 操作时,你所要做的就是使用IO 上的liftIO 实例方法将它提升到组合的monad 堆栈中。

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

【讨论】:

  • 谢谢,这似乎正是我所需要的。不幸的是,我还没有 Scalaz 技能来理解它是如何工作的 :( 你能提供一个简单的例子来说明如何使用它们,使用我上面给出的方法签名吗?
  • @SebastienLorber 一个 Scalaz 提示。尝试自己重新实现一些东西以更好地了解它们的工作原理是个好主意。在这种情况下,为 EitherT 实现 Monad 实例是一个非常好的练习。
  • 当我执行建议的导入时,我得到“对象效果不是包 scalaz 的成员”。
  • @JamesMoore 你需要确保scalaz-corescalaz-effect 都在类路径中。
  • 非常好的答案!有没有一种简单的方法可以将 EitherT[IO, E, A] 转换为 IO[Either[E, A]] 并在方法签名中使用它?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-16
  • 1970-01-01
  • 2021-04-28
  • 2022-09-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多