Scala中是否有收集独立计算序列全部错误信息的通用模式
解决方案
Scala标准库的Try是Monad类型,天生带短路特性,for推导基于flatMap实现,只要前序步骤失败就会终止后续执行,无法收集后续错误。要实现错误聚合,有两类常用实现方案:
方案1:使用Cats库实现(生产环境推荐)
Cats库提供的Validated类型基于Applicative计算模型,会执行所有独立计算再汇总结果,语法和原写法几乎一样简洁:
import cats._ import cats.data._ import cats.implicits._ import scala.util.{Try, Success, Failure} // 定义业务实体 case class Sauce(name: String) case class Crust(name: String) case class Cheese(name: String) case class Pizza(sauce: Sauce, crust: Crust, cheese: Cheese) // 原有业务方法 def makeSauce(): Try[Sauce] = Success(Sauce("marinara")) def makeCrust(): Try[Crust] = Failure(new RuntimeException("stale bread")) def makeCheese(): Try[Cheese] = Failure(new IllegalArgumentException("moldy cheese")) // 转为Validated类型,错误存入非空列表用于聚合 def makeSauceV: ValidatedNec[Throwable, Sauce] = makeSauce().toValidatedNec def makeCrustV: ValidatedNec[Throwable, Crust] = makeCrust().toValidatedNec def makeCheeseV: ValidatedNec[Throwable, Cheese] = makeCheese().toValidatedNec
聚合所有计算结果,自动收集全部错误:
// 所有计算成功则返回Pizza,失败则返回所有错误的集合 val result: ValidatedNec[Throwable, Pizza] = (makeSauceV, makeCrustV, makeCheeseV).mapN(Pizza.apply) // 如果需要转为带复合错误的Try对象 val tryResult: Try[Pizza] = result .leftMap(errors => new RuntimeException(errors.map(_.getMessage).mkString_("Multiple errors: ", ", ", ""))) .toTry
如果需要并行执行所有计算进一步提升效率,可以用Cats Effect的parMapN语法,写法完全一致:
import cats.effect.IO import cats.effect.implicits._ def makeSauceIO: IO[Sauce] = IO.fromTry(makeSauce()) def makeCrustIO: IO[Crust] = IO.fromTry(makeCrust()) def makeCheeseIO: IO[Cheese] = IO.fromTry(makeCheese()) // 并行执行三个独立计算,自动聚合错误 val pizzaIO: IO[Pizza] = (makeSauceIO, makeCrustIO, makeCheeseIO).parMapN(Pizza.apply)
方案2:原生Scala实现(无需第三方依赖)
如果不想引入外部库,可以自己封装工具方法收集所有Try的错误:
import scala.util.{Try, Success, Failure} // 工具方法:聚合所有Try的错误,全成功才返回成功结果 def sequenceTryAggregateErrors[T](trys: List[Try[_]])(successFactory: => T): Try[T] = { val failures = trys.collect { case Failure(e) => e } if (failures.isEmpty) Success(successFactory) else Failure(new RuntimeException(s"Aggregated errors: ${failures.map(_.getMessage).mkString(", ")}")) } // 使用方式和原有写法非常接近 val pizzaTry: Try[Pizza] = sequenceTryAggregateErrors( List(makeSauce(), makeCrust(), makeCheese()) )(Pizza(makeSauce().get, makeCrust().get, makeCheese().get))
注意事项
- 错误聚合仅适用于互相独立的计算,如果后序计算依赖前序计算的结果,还是需要用原来的for推导做顺序执行
- 错误聚合会执行所有传入的计算,哪怕前面的计算已经失败,如果计算包含副作用需要提前评估影响
内容的提问来源于stack exchange,提问作者Sasgorilla
相关产品推荐
相关产品推荐

