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 works:
val schoolFuture = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- schoolStore.getSchool(sid.get) if sid.isDefined
} yield s
However I'm not happy with having the "if" in there, it seems that I should be able to use a map instead.
But when I try with a map:
val schoolFuture: Future[Option[School]] = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- sid.map(schoolStore.getSchool(_))
} yield s
I get a compile error:
[error] found : Option[scala.concurrent.Future[Option[School]]]
[error] required: scala.concurrent.Future[Option[School]]
[error] s <- sid.map(schoolStore.getSchool(_))
I've played around with a few variations, but haven't found anything attractive that works. Can anyone suggest a nicer comprehension and/or explain what's wrong with my 2nd example?
Here is a minimal but complete runnable example with Scala 2.10:
import concurrent.{Future, Promise}
case class User(userId: Int)
case class UserDetails(userId: Int, schoolId: Option[Int])
case class School(schoolId: Int, name: String)
trait Error
class UserStore {
def getUserDetails(userId: Int): Future[Either[Error, UserDetails]] = Promise.successful(Right(UserDetails(1, Some(1)))).future
}
class SchoolStore {
def getSchool(schoolId: Int): Future[Option[School]] = Promise.successful(Option(School(1, "Big School"))).future
}
object Demo {
import concurrent.ExecutionContext.Implicits.global
val userStore = new UserStore
val schoolStore = new SchoolStore
val user = User(1)
val schoolFuture: Future[Option[School]] = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- sid.map(schoolStore.getSchool(_))
} yield s
}
It's easier to use
https://github.com/qifun/stateless-future
orhttps://github.com/scala/async
to doA-Normal-Form
transform.What behavior would you like to occur in the case that the
Option[School]
isNone
? 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 thefilter
method. The contract onFuture#filter
is thus:But wait:
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:Keep in mind that the result of
schoolFuture
can be in instance ofscala.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
andOption
don't compose insidefor
because there aren't the correctflatMap
signatures. As a reminder, for desugars like so:(where any
if
statement throws afilter
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 onlyflatMap
otherFuture
s, every statementc0
,c1
, ... except the last had better produce aFuture
.Now,
getUserDetails
andgetSchool
both produceFutures
, butsid
is anOption
, 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. Ifo
is an option, we canto turn an
Option
into an already-completedFuture
. Sowill do the trick. Is that better than what you've got? Doubtful. But if you
then suddenly the for-comprehension looks reasonable again:
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 ofFuture
; I'd suggest that your original code (which invokes a filter) is at least as good of a way to do it.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.)
This answer to a similar question about
Promise[Option[A]]
might help. Just substituteFuture
forPromise
.I'm inferring the following types for
getUserDetails
andgetSchool
from your question:Since you ignore the failure value from the
Either
, transforming it to anOption
instead, you effectively have two values of typeA => Future[Option[B]]
.Once you've got a
Monad
instance forFuture
(there may be one in scalaz, or you could write your own as in the answer I linked), applying theOptionT
transformer to your problem would look something like this: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 typeFuture[Option[SchoolID]]
with the transformer'srun
method.