I have two functions which return Futures. I\'m trying to feed a modified result from first function into the other using a for-yield comprehension.
This approach w
It's easier to use https://github.com/qifun/stateless-future
or https://github.com/scala/async
to do A-Normal-Form
transform.
This answer to a similar question about Promise[Option[A]]
might help. Just substitute Future
for Promise
.
I'm inferring the following types for getUserDetails
and getSchool
from your question:
getUserDetails: UserID => Future[Either[??, UserDetails]]
getSchool: SchoolID => Future[Option[School]]
Since you ignore the failure value from the Either
, transforming it to an Option
instead, you effectively have two values of type A => Future[Option[B]]
.
Once you've got a Monad
instance for Future
(there may be one in scalaz, or you could write your own as in the answer I linked), applying the OptionT
transformer to your problem would look something like this:
for {
ud <- optionT(getUserDetails(user.userID) map (_.right.toOption))
sid <- optionT(Future.successful(ud.schoolID))
s <- optionT(getSchool(sid))
} yield s
Note that, to keep the types compatible, ud.schoolID
is wrapped in an (already completed) Future.
The result of this for-comprehension would have type OptionT[Future, SchoolID]
. You can extract a value of type Future[Option[SchoolID]]
with the transformer's run
method.
We've made small wrapper on Future[Option[T]] which acts like one monad (nobody even checked none of monad laws, but there is map, flatMap, foreach, filter and so on) - MaybeLater. It behaves much more than an async option.
There are a lot of smelly code there, but maybe it will be usefull at least as an example. BTW: there are a lot of open questions(here for ex.)
What behavior would you like to occur in the case that the Option[School]
is None
? Would you like the Future to fail? With what kind of exception? Would you like it to never complete? (That sounds like a bad idea).
Anyways, the if
clause in a for-expression desugars to a call to the filter
method. The contract on Future#filter
is thus:
If the current future contains a value which satisfies the predicate, the new future will also hold that value. Otherwise, the resulting future will fail with a NoSuchElementException.
But wait:
scala> None.get
java.util.NoSuchElementException: None.get
As you can see, None.get returns the exact same thing.
Thus, getting rid of the if sid.isDefined
should work, and this should return a reasonable result:
val schoolFuture = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- schoolStore.getSchool(sid.get)
} yield s
Keep in mind that the result of schoolFuture
can be in instance of scala.util.Failure[NoSuchElementException]
. But you haven't described what other behavior you'd like.
(Edited to give a correct answer!)
The key here is that Future
and Option
don't compose inside for
because there aren't the correct flatMap
signatures. As a reminder, for desugars like so:
for ( x0 <- c0; w1 = d1; x1 <- c1 if p1; ... ; xN <- cN) yield f
c0.flatMap{ x0 =>
val w1 = d1
c1.filter(x1 => p1).flatMap{ x1 =>
... cN.map(xN => f) ...
}
}
(where any if
statement throws a filter
into the chain--I've given just one example--and the equals statements just set variables before the next part of the chain). Since you can only flatMap
other Future
s, every statement c0
, c1
, ... except the last had better produce a Future
.
Now, getUserDetails
and getSchool
both produce Futures
, but sid
is an Option
, so we can't put it on the right-hand side of a <-
. Unfortunately, there's no clean out-of-the-box way to do this. If o
is an option, we can
o.map(Future.successful).getOrElse(Future.failed(new Exception))
to turn an Option
into an already-completed Future
. So
for {
ud <- userStore.getUserDetails(user.userId) // RHS is a Future[Either[...]]
sid = ud.right.toOption.flatMap(_.schoolId) // RHS is an Option[Int]
fid <- sid.map(Future.successful).getOrElse(Future.failed(new Exception)) // RHS is Future[Int]
s <- schoolStore.getSchool(fid)
} yield s
will do the trick. Is that better than what you've got? Doubtful. But if you
implicit class OptionIsFuture[A](val option: Option[A]) extends AnyVal {
def future = option.map(Future.successful).getOrElse(Future.failed(new Exception))
}
then suddenly the for-comprehension looks reasonable again:
for {
ud <- userStore.getUserDetails(user.userId)
sid <- ud.right.toOption.flatMap(_.schoolId).future
s <- schoolStore.getSchool(sid)
} yield s
Is this the best way to write this code? Probably not; it relies upon converting a None
into an exception simply because you don't know what else to do at that point. This is hard to work around because of the design decisions of Future
; I'd suggest that your original code (which invokes a filter) is at least as good of a way to do it.