The central problem is that for-understanding, they can only work on one monad at a time, in which case it is a Future monad and the only way to short-circuit the sequence of future calls is that the future will fail. This works because subsequent calls in the understanding, these are just calls to map and flatmap , and the behavior of map / flatmap if Future is unsuccessful is to return this future and not execute the provided body (i.e. the called function).
What you are trying to achieve is a summary of a workflow based on certain conditions, and not do it if not in the future. This can be done by wrapping the result in another container, call it Result[A] , which gives an understanding of the type Future[Result[A]] . Result will either contain the value of the result or be the final result. The task is as follows:
- provides subsequent function calls contained in a previous non-exhaustive
Result - prevent subsequent function call if
Result completes
map/flatmap seems to be candidates for these types of compositions, except that we will have to call them manually, since the only map/flatmap that can evaluate for-comprehension is the result that leads to Future[Result[A]] .
Result can be defined as:
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)) } }
For each call, an action is indeed a potential action, since calling it or with a final result will simply support the final result. Note: Terminator is Result[Nothing] , as it will never contain values, and any Result[+A] can be Result[Nothing] .
The end result is defined as:
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 }
The end result allows short-circuited calls to functions that require the value [A] when we have already met our termination condition.
A non-limiting result is defined as:
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 non-mathematical result allows us to provide functions [A] . For good measure, I also predefined UnitResult for functions that are purely secondary, such as goodDao.removeGood .
Now let's define your good but final conditions:
case object UserNotFound extends Terminator case object NotAuthenticated extends Terminator case object GoodNotFound extends Terminator case object NoOwnership extends Terminator
We now have the tools to create the workflow you were looking for. Everyone needs a function to understand, which returns a Future[Result[A]] on the right side, creating Result[A] on the left side. flatmap on Result[A] allows you to call (or short-circuited) a function that requires input [A] , and we can then map it to a new Result :
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 }
I know that for any Future for an understanding that has completion conditions, a lot of settings can be used, but at least the Result type.