Future[Option] in Scala for-comprehensions
Solution 1
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.
Solution 2
(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.
Solution 3
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.
Solution 4
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.)
Solution 5
It's easier to use https://github.com/qifun/stateless-future
or https://github.com/scala/async
to do A-Normal-Form
transform.
Ryan Bair
Updated on January 20, 2020Comments
-
Ryan Bair over 4 years
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 }
-
Ryan Bair over 11 yearsThis is giving me a compile error. found : scala.concurrent.Future[Option[com.authorpub.userservice.School]] required: Option[?] s <- schoolStore.getSchool(sid)
-
Rex Kerr over 11 yearsCan you explain what all your types are? It's a little hard to tell given that you haven't posted full working code. What is a future and what is an option in your working code? You can get Scala 2.10 to print out the type of an expression if you do this:
import scala.reflect.runtime.universe._; def typeme[A: TypeTag](a: A) = { println(implicitly[TypeTag[A]]); a }
and then wrap the expression intypeme
, e.g.sid = typeme(ud.right.toOption.flatMap(_.schoolID))
. -
Rex Kerr over 11 yearsSomeone downvoted without comment. This isn't very useful. What's wrong with the answer as it stands now?
-
Ryan Bair over 11 yearsAdmittedly scalaz scares me a bit, I still have a ways to go on the learning curve. This solves the issue about as well as I think it can be. Thanks!
-
iwein about 10 years+1 for a solution that doesn't need Scalaz ;) ducks
-
Luca Molteni almost 9 yearsThank you for the answer, but the link to the scalaz-contrib library is broken
-
Luca Molteni almost 9 yearsYou don't need
scalaz-contrib anymore
, since future instances for Monad are now provided by scalaz itself by mixing theFutureInstances
trait. -
ps_ttf about 8 yearsI prefer the answer from Ben James. Although Scalaz, Monads and all this stuff scares many people, in fact these concepts are very simple. Scalaz already has all the abstractions needed to solve the problem. On the contrary, this answer introduces a new concept
OptionIsFuture
which I suppose to be similar to a monad transformer.