Scala-way处理for-comprehensions的条件?
我正在尝试创建一个整洁的结构,以便理解基于未来的业务逻辑.这是一个示例,其中包含基于异常处理的工作示例:
(for { // find the user by id,findUser(id) returns Future[Option[User]] userOpt <- userDao.findUser(userId) _ = if (!userOpt.isDefined) throw new EntityNotFoundException(classOf[User],userId) user = userOpt.get // authenticate it,authenticate(user) returns Future[AuthResult] authResult <- userDao.authenticate(user) _ = if (!authResult.ok) throw new AuthFailedException(userId) // find the good owned by the user,findGood(id) returns Future[Option[Good]] goodOpt <- goodDao.findGood(goodId) _ = if (!good.isDefined) throw new EntityNotFoundException(classOf[Good],goodId) good = goodOpt.get // check ownership for the user,checkOwnership(user,good) returns Future[Boolean] ownership <- goodDao.checkOwnership(user,good) if (!ownership) throw new OwnershipException(user,good) _ <- goodDao.remove(good) } yield { renderJson(Map( "success" -> true )) }) .recover { case ex: EntityNotFoundException => /// ... handle error cases ... renderJson(Map( "success" -> false,"error" -> "Your blahblahblah was not found in our database" )) case ex: AuthFailedException => /// ... handle error cases ... case ex: OwnershipException => /// ... handle error cases ... } 但是,这可能被视为处理事物的非功能性或非Scala方式.有一个更好的方法吗? 请注意,这些错误来自不同的来源 – 有些属于业务级别(‘检查所有权’),有些属于控制器级别(‘授权’),有些属于数据库级别(‘未找到实体’).因此,从单个常见错误类型派生它们时的方法可能不起作用. 解决方法
主要的挑战是,for-comprehensions一次只能在一个monad上工作,在这种情况下它是Future monad,并且短路未来调用序列的唯一方法是将来失败.这是有效的,因为for-comprehension中的后续调用只是map和flatmap调用,而失败的Future上的map / flatmap的行为是返回该未来并且不执行提供的主体(即被调用的函数).
您要实现的目标是基于某些条件的工作流程的短暂启动,而不是未来的失败.这可以通过将结果包装在另一个容器中来完成,让我们称之为Result [A],它为理解提供了一种Future [Result [A]].结果将包含结果值,或者是终止结果.面临的挑战是如何: >提供后续函数调用先前非终止结果包含的值 map / flatmap看起来像是做这些类型的合成的候选者,除了我们必须手动调用它们,因为for-comprehension可以评估的唯一map / flatmap是导致Future [Result [A]]的地图/ flatmap. 结果可以定义为: trait Result[+A] { // the intermediate Result def value: A // convert this result into a final result based on another result def given[B](other: Result[B]): Result[A] = other match { case x: Terminator => x case v => this } // replace the value of this result with the provided one def apply[B](v: B): Result[B] // replace the current result with one based on function call def flatMap[A2 >: A,B](f: A2 => Future[Result[B]]): Future[Result[B]] // create a new result using the value of both def combine[B](other: Result[B]): Result[(A,B)] = other match { case x: Terminator => x case b => Successful((value,b.value)) } } 对于每个调用,该操作实际上是一个潜在的操作,因为调用它或使用终止结果,将简单地保持终止结果.请注意,Terminator是Result [Nothing],因为它永远不会包含值,并且任何Result [A]都可以是Result [Nothing]. 终止结果定义为: sealed trait Terminator extends Result[Nothing] { val value = throw new IllegalStateException() // The terminator will always short-circuit and return itself as // the success rather than execute the provided block,thus // propagating the terminating result def flatMap[A2 >: Nothing,B](f: A2 => Future[Result[B]]): Future[Result[B]] = Future.successful(this) // if we apply just a value to a Terminator the result is always the Terminator def apply[B](v: B): Result[B] = this // this apply is a convenience function for returning this terminator // or a successful value if the input has some value def apply[A](opt: Option[A]) = opt match { case None => this case Some(v) => Successful[A](v) } // this apply is a convenience function for returning this terminator or // a UnitResult def apply(bool: Boolean): Result[Unit] = if (bool) UnitResult else this } 当我们已经满足终止条件时,终止结果可以使对需要值[A]的函数的调用短路. 非终止结果定义为: trait SuccessfulResult[+A] extends Result[A] { def apply[B](v: B): Result[B] = Successful(v) def flatMap[A2 >: A,B](f: A2 => Future[Result[B]]): Future[Result[B]] = f(value) } case class Successful[+A](value: A) extends SuccessfulResult[A] case object UnitResult extends SuccessfulResult[Unit] { val value = {} } 非终止结果使得可以向函数提供包含的值[A].为了更好的衡量,我还为纯粹副作用的函数预定了一个UnitResult,比如goodDao.removeGood. 现在让我们来定义你的好但终止的条件: case object UserNotFound extends Terminator case object NotAuthenticated extends Terminator case object GoodNotFound extends Terminator case object NoOwnership extends Terminator 现在我们有了创建您正在寻找的工作流程的工具.每个for comprehention想要一个函数,它返回右侧的Future [Result [A]],在左侧产生Result [A].结果[A]上的flatMap可以调用(或短路)需要[A]作为输入的函数,然后我们可以将其结果映射到新结果: def renderJson(data: Map[Any,Any]): JsResult = ??? def renderError(message: String): JsResult = ??? val resultFuture = for { // apply UserNotFound to the Option to conver it into Result[User] or UserNotFound userResult <- userDao.findUser(userId).map(UserNotFound(_)) // apply NotAuthenticated to AuthResult.ok to create a UnitResult or NotAuthenticated authResult <- userResult.flatMap(user => userDao.authenticate(user).map(x => NotAuthenticated(x.ok))) goodResult <- authResult.flatMap(_ => goodDao.findGood(goodId).map(GoodNotFound(_))) // combine user and good,so we can feed it into checkOwnership comboResult = userResult.combine(goodResult) ownershipResult <- goodResult.flatMap { case (user,good) => goodDao.checkOwnership(user,good).map(NoOwnership(_))} // in order to call removeGood with a good value,we take the original // good result and potentially convert it to a Terminator based on // ownershipResult via .given _ <- goodResult.given(ownershipResult).flatMap(good => goodDao.removeGood(good).map(x => UnitResult)) } yield { // ownership was the last result we cared about,so we apply the output // to it to create a Future[Result[JsResult]] or some Terminator ownershipResult(renderJson(Map( "success" -> true ))) } // now we can map Result into its value or some other value based on the Terminator val jsFuture = resultFuture.map { case UserNotFound => renderError("User not found") case NotAuthenticated => renderError("User not authenticated") case GoodNotFound => renderError("Good not found") case NoOwnership => renderError("No ownership") case x => x.value } 我知道这是一个很多设置,但至少Result类型可以用于任何具有终止条件的Future for-comprehension. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |