尝试[结果]、IO[结果]、Either[错误,结果],最终我应该使用哪一个?(这是一个提问标题)

23

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

这个问题有点像我之前在Scala中关于错误处理的许多问题的总结。你可以在这里找到一些问题:


目前我理解以下内容:

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

仓库层

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

  • 致命故障(OutOfMemoryError
  • IO故障,因为数据库无法访问/可读

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

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

由于我们正在处理IO操作,因此如果我们想要纯函数,则IO单子也是有意义的。

因此,结果类型可能是:

  • Try [Option [User]]
  • IO [Option [User]]
  • 其他?

服务层

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

可能会发生以下故障:

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

然后结果类型可以是:

  • Try[Either[BusinessError,User]]
  • IO[Either[BusinessError,User]]
  • 其他什么?

这里的BusinessError不是Throwable,因为它不是异常故障。


使用for-comprehensions

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

我们不能轻松地在for-comprehension中混合不同的monads,所以我想我应该为所有操作定义一种统一的返回类型,对吗?

我只是想知道,在你们真实的Scala应用程序中,当出现不同类型的故障时,如何成功地保持使用for-comprehensions。

目前,for-comprehension对我来说效果很好,使用服务和存储库,它们都返回Either [Error,Result],但是所有不同类型的故障都融合在一起,处理这些故障变得有点繁琐。

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

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

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


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

1
你能否提供一些代码,展示你是如何编写这个程序的?我非常感兴趣,因为我也有类似的想法。我的scalaz技能还不够熟练,我很难理解这些类的含义。 - boggy
2个回答

17
这就是 Scalaz 的 EitherT monad transformer 的作用。一堆 IO[Either[E, A]] 等同于 EitherT[IO, E, A],不同的是前者必须按顺序处理多个单独的单子,而后者自动成为一个添加了基础单子 IOEither 功能的单子。同样,您可以使用 EitherT[Future, E, A] 将非异常错误处理添加到异步操作中。
通常,单子变换器是将多个单子混合在单个 for-推断和/或单子操作中的答案。
编辑: 我将假设您正在使用 Scalaz 版本 7.0.0。
要在 IO 单子之上使用 EitherT 单子变换器,您首先需要导入 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]

在每个方法中,您可以使用基于EitherTIO的单一、统一的monad堆栈,像往常一样在for推导式中使用。 EitherT将贯穿整个计算的基本monad(在本例中为IO),同时处理错误,方式与Either通常相同(默认情况下已经是右偏的,因此您不必不断处理所有通常的.right垃圾)。当您想要执行IO操作时,您只需通过在IO上使用liftIO实例方法将其提升到组合的monad堆栈中。

顺便说一句,以这种方式工作时,EitherT伴生对象中的函数非常有用。


2
@SebastienLorber 一个 Scalaz 的技巧。尝试自己重新实现一些东西以更好地理解它们的工作原理是一个好主意。在这种情况下,为 EitherT 实现 Monad 实例是一个非常好的练习。 - Eric
1
@JamesMoore 你需要确保 scalaz-corescalaz-effect 都在类路径上。 - Ptharien's Flame
1
非常好的回答!有没有一种简单的方法可以将 EitherT[IO,E,A] 转换为 IO[Either[E,A]] 并在方法签名中使用它? - Chris W.
1
@Ptharien's Flame:感谢你的提示,我没有表述得够清楚。在我的解决方案中(请参见https://dev59.com/5XbZa4cB1Zd3GeqPK-P4),我使用了\/(ScalaZ的Either),但奇怪的是我只能将EitherT转换为scala.util.Either而不能转回\/。你有解释或简单的解决方案吗? - Chris W.
1
@Ptharien's Flame:我已经找到了:它叫做 run。没想到会有这样的方法名。 - Chris W.
显示剩余5条评论

11
更新2 [2020-09]: 自从这个答案第一次被编辑以来,Scala生态系统发生了一些变化。cat-effect 3 讨论了拥有专用错误通道[更新2021-03:最终选择不这样做],scalaz 8 停滞不前,从中出现了一个新的库:ZIO,这个库将一个双函子IO单子(+一个超出本题范围的依赖注入系统)作为其核心,在Scala中越来越受欢迎。

由于它有一个专门的错误通道,刚刚发布了v1.0.0版本,并且该主题仍然很新颖,我回答了与此相关的问题:什么是ZIO错误通道,如何对其中要放置的内容有所了解?

它还涉及到更一般的问题(例如:发现应用程序的故障模式并处理它们,以便未来的开发人员/管理员/用户可以在错误情况下管理行为),并简要总结了我的演讲中的系统化错误管理。希望它能对这个(庞大而复杂的)主题提供更多背景。


@pthariens-flame的答案非常好,你应该把它用于手头的任务。

我想提供一些背景信息,让大家了解最近这个领域的发展情况,所以这只是一个一般性的信息回答。
错误管理基本上是我们开发人员的主要工作。顺畅的路径很愉快但也很无聊,并且用户不会在那里抱怨。大多数(全部?)问题出现在过程中涉及到效果(尤其是I/O)的地方。
其中一种处理这些问题的方法是遵循通常称为“纯FP方法”的方法,在程序的纯/总体和不纯/非总体部分之间划定一条大红线。这样做可以利用净化效果的可能性来清洁地处理错误,具体取决于它们的类型。
最近18个月以来,Scala在这个领域进行了大量的研究和开发。实际上,我认为Scala今天在所有语言中对这个特定问题是最令人兴奋和有颠覆性的地方(当然,这很可能只是可用性/最新信息的大脑偏见)。
Scalaz8、Monix和cats-effects是这种快速演变的三个主要贡献者。因此,与这三个项目相关的任何内容(会议演讲,博客文章等)都将帮助您了解正在发生的事情。
因此,为了简短起见,Scalaz8将改变IO建模方式,以更好地考虑错误管理。John DeGoes在这方面领导着努力,并且他在这个主题上制作了一些很好的资源:
文章:
  • 《再见Transformers: Scalaz 8中的高性能效果》http://degoes.net/articles/effects-without-transformers
  • 双函子IO:距离动态类型的错误处理只有一步之遥http://degoes.net/articles/bifunctor-io
  • 视频:

    Monix和Cats-effect也有很多值得关注的内容,但我认为大部分相关资源都在相应项目的拉取请求中。

    以下是Alexandru Nedelcu的一次演讲,介绍了问题的背景:

    Adam Warski在这里做了一个对比:

    最后,对于 Cats 部分,Luka Jacobowitz 写了一篇绝佳的文章:“重新思考 MonadError” https://typelevel.org/blog/2018/04/13/rethinking-monaderror.html,涵盖了很多相同的内容但用另一种方式表述。

    [编辑]: 正如同事们注意到的那样,在 Scala 领域内,(r)evolution 的范围不仅止于此。有很大的工作正在尝试使效果编码(包括 IO 等)更具性能。该领域的最新进展是尝试使用 Kleisli Arrow 代替单子以最小化 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战争:FP-OOP vs FP”http://degoes.net/articles/fpoop-vs-fp


    当使用cats effects IO时,今天的最新技术是什么? - MaatDeamon
    据我所知,cats-effect 3 在错误管理方面并没有改变一般架构(另一方面,它在纤维方面是一个重大的进步)。 - fanf42
    谢谢你!在 Scala 函数式编程世界中,对于错误管理确实存在不同的哲学分歧!Zio 和 Cats。 - MaatDeamon

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接