首页 > 解决方案 > 如何对一系列失败执行折叠操作

问题描述

我们的代码库中有一些遗留代码,最终将被重构为使用 Cats 库中的 Validated 和 Either。这是因为 Validated 不使用快速失败机制。未重构的代码使用 Try monad 的快速失败机制。

由于重构还没有发生,我正在做一个笨拙的黑客来解决 Try monad 快速失败的事实。但是,我在实施它时遇到了麻烦。

我基本上有一个 Try[T] 类型的列表,它保证都是失败的。

我正在尝试将所有失败的所有错误消息聚合到一个失败中。

这是我正在重构的功能:

  private def extractTry[T](xs: IndexedSeq[Try[T]]): Try[IndexedSeq[T]] = {
    val failures = xs.collect { case Failure(ex) => Failure(ex) }
    if (failures.size > 0) failures.head
    else Success(xs.map(_.get))
  }

我想聚合所有失败,而不是方法第二行中的 failures.head。

所以像

if (failures.size > 0) failures.foldLeft(Failure(new IllegalArgumentException(""))){case (Failure(acc), Failure(e)) => Failure(new IllegalArgumentException(acc.getMessage + e.getMessage))} 

我唯一不喜欢这个实现的地方是我希望折叠的每一步都不要使用 IllegalArgumentException,而是使用新元素的异常类型。所以想法是在失败中保持最后一个元素的异常类型,而不是使用任意异常类型。

我们计划最终使用 Either[Throwable, T] 代替 Try 并且当我们尝试聚合错误时可能会遇到完全相同的问题。我们希望保留异常类型,而不是像 IllegalArgumentException 那样指定任意类型。所以这个问题迟早要解决,我宁愿早点解决。

有没有人有什么建议?任何帮助,将不胜感激。

标签: scalaexceptionfunctional-programmingmonads

解决方案


理想情况下,我们会遵循@Luis 的建议。在那之前考虑可能是这样的

sealed trait OverallResult[+T]
case class OverallError(accumulatedMessage: String, finalErrorCode: Int) extends OverallResult[Nothing]
case class OverallSuccess[T](xs: IndexedSeq[T]) extends OverallResult[T]

object OverallResult {
  /**
   * Aggregating over a chain of Failures, it will only keep the exception type of the last Failure.
   * This is just a heuristic to decide on the error code. Depending on the exception type, we use
   * a different error code. So NoSuchElementException is 404 and IllegalArgumentException is 400.
   */
  def apply[T](xs: IndexedSeq[Try[T]]): OverallResult[T] = {
    val failures = xs.collect { case Failure(ex) => ex }
    if (failures.nonEmpty) {
      val accMessage = failures.map(_.getMessage).mkString("[", ",", "]")
      OverallError(accMessage, errorCode(failures.last))
    }
    else OverallSuccess(xs.map(_.get))
  }

  private def errorCode(ex: Throwable): Int = ex match {
    case _: NoSuchElementException => 404
    case _: IllegalArgumentException => 400
    case e => throw new RuntimeException("Unexpected exception. Fix ASAP!", e)
  }
}

OverallResult(Vector(Try(throw new NoSuchElementException("boom")), Try(throw new IllegalArgumentException("crash"))))
OverallResult(Vector(Try(42), Try(11)))

哪个输出

res0: OverallResult[Nothing] = OverallError([boom,crash],400)
res1: OverallResult[Int] = OverallSuccess(Vector(42, 11))

请注意评论中提到的启发式的明确文档:

/**
  * Aggregating over a chain of Failures, it will only keep the exception type of the last Failure.
  * This is just a heuristic to decide on the error code. Depending on the exception type, we use
  * a different error code. So NoSuchElementException is 404 and IllegalArgumentException is 400.
  */

误差累积与模拟

failures.map(_.getMessage).mkString("[", ",", "]")

和整体状态码决定

errorCode(failures.last)

现在extractTry需要重构客户端以在OverallResultADT 上进行模式匹配,而finalErrorCode不是异常,但较低级别的代码库应该不受影响。


推荐阅读