I would say that the first problem with this design is that the Future blocking call port should not wrap an Option type:
Blocking call: def giveMeSomethingBlocking(for:Id): Option[T] Should be: def giveMeSomethingBlocking(for:Id): Future[T] And not: def giveMeSomethingBlocking(for:Id): Future[Option[T]]
A blocking call gives either Some(value) or None , a non-blocking version of Future gives either Success(value) or Failure(exception) , which fully retains the semantics of Option non-blocking way.
With this in mind, we can simulate the process in question using combinators on Future . Let's see how:
First, let's refactor the API with something we can work with:
type UserPoints = Long object db { def getUserPoints(username: String): Future[UserPoints] = ??? def getSomething(s: String): Future[UserPoints] = ??? def setPoints(pointId:UserPoints, username: String): Future[Unit] = ??? def findPointsForUser(username: String): Future[UserPoints] = ??? } class PointsNotFound extends Exception("bonk") class StuffNotFound extends Exception("sthing not found")
Then the process will look like this:
def getUserPoints(username:String): Future[UserPoints] = { db.getUserPoints(username) .map(userPoints => userPoints ) .recoverWith{ case ex:PointsNotFound => (for { sthingElse <- db.getSomething("abc") _ <- db.setPoints(sthingElse, username) points <- db.findPointsForUser(username) } yield (points)) .recoverWith{ case ex: StuffNotFound => db.findPointsForUser(username) } } }
Which type of validation is correct.
Edit
Given that the API is installed on stone, the way to deal with nested monadic types is to define MonadTransformer. In simple words, make Future[Option[T]] new monad, call it FutureO , which can be composed with others of its kind. [one]
case class FutureO[+A](future: Future[Option[A]]) { def flatMap[B](f: A => FutureO[B])(implicit ec: ExecutionContext): FutureO[B] = { val newFuture = future.flatMap{ case Some(a) => f(a).future case None => Future.successful(None) } FutureO(newFuture) } def map[B](f: A => B)(implicit ec: ExecutionContext): FutureO[B] = { FutureO(future.map(option => option map f)) } def recoverWith[U >: A](pf: PartialFunction[Throwable, FutureO[U]])(implicit executor: ExecutionContext): FutureO[U] = { val futOtoFut: FutureO[U] => Future[Option[U]] = _.future FutureO(future.recoverWith(pf andThen futOtoFut)) } def orElse[U >: A](other: => FutureO[U])(implicit executor: ExecutionContext): FutureO[U] = { FutureO(future.flatMap{ case None => other.future case _ => this.future }) } }
And now we can rewrite our process, maintaining the same structure as the future composition.
type UserPoints = Long object db { def getUserPoints(username: String): Future[Option[UserPoints]] = ??? def getSomething(s: String): Future[Option[Long]] = ??? def setPoints(pointId: UserPoints, username:String): Future[Unit] = ??? def findPointsForUser(username: String): Future[Option[Long]] = ??? } class PointsNotFound extends Exception("bonk") class StuffNotFound extends Exception("sthing not found") def getUserPoints2(username:String): Future[Option[UserPoints]] = { val futureOpt = FutureO(db.getUserPoints(username)) .map(userPoints => userPoints ) .orElse{ (for { sthingElse <- FutureO(db.getSomething("abc")) _ <- FutureO(db.setPoints(sthingElse, username).map(_ => Some(()))) points <- FutureO(db.findPointsForUser(username)) } yield (points)) .orElse{ FutureO(db.findPointsForUser(username)) } } futureOpt.future }
[1] with confirmation http://loicdescotte.imtqy.com/posts/scala-compose-option-future/