J'ai deux fonctions qui renvoient Futures. J'essaie d'introduire un résultat modifié de la première fonction dans l'autre en utilisant une compréhension du rendement.
Cette approche fonctionne:
val schoolFuture = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- schoolStore.getSchool(sid.get) if sid.isDefined
} yield s
Cependant, je ne suis pas content d'avoir le "si" là-dedans, il semble que je devrais pouvoir utiliser une carte à la place.
Mais quand j'essaye avec une carte:
val schoolFuture: Future[Option[School]] = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- sid.map(schoolStore.getSchool(_))
} yield s
J'obtiens une erreur de compilation:
[error] found : Option[scala.concurrent.Future[Option[School]]]
[error] required: scala.concurrent.Future[Option[School]]
[error] s <- sid.map(schoolStore.getSchool(_))
J'ai joué avec quelques variations, mais je n'ai rien trouvé d'attrayant qui fonctionne. Quelqu'un peut-il suggérer une meilleure compréhension et/ou expliquer ce qui ne va pas avec mon 2ème exemple?
Voici un exemple exécutable minimal mais complet avec 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
}
Cette réponse à une question similaire sur Promise[Option[A]]
pourrait aider. Remplacez simplement Future
par Promise
.
Je déduis les types suivants pour getUserDetails
et getSchool
de votre question:
getUserDetails: UserID => Future[Either[??, UserDetails]]
getSchool: SchoolID => Future[Option[School]]
Puisque vous ignorez la valeur d'échec de Either
, en la transformant en Option
à la place, vous avez effectivement deux valeurs de type A => Future[Option[B]]
.
Une fois que vous avez une instance de Monad
pour Future
(il peut y en avoir une dans scalaz , ou vous pouvez écrire la vôtre comme dans la réponse que j'ai liée), en appliquant le transformateur OptionT
de votre problème ressemblerait à ceci:
for {
ud <- optionT(getUserDetails(user.userID) map (_.right.toOption))
sid <- optionT(Future.successful(ud.schoolID))
s <- optionT(getSchool(sid))
} yield s
Notez que, pour garder les types compatibles, ud.schoolID
est enveloppé dans un avenir (déjà terminé).
Le résultat de cette incompréhension aurait le type OptionT[Future, SchoolID]
. Vous pouvez extraire une valeur de type Future[Option[SchoolID]]
avec la méthode run
du transformateur.
(Modifié pour donner une réponse correcte!)
La clé ici est que Future
et Option
ne composez pas à l'intérieur de for
car il n'y a pas les signatures flatMap
correctes . Pour rappel, pour des desugars comme ça:
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) ...
}
}
(où toute instruction if
jette un filter
dans la chaîne - je n'ai donné qu'un exemple - et les instructions égales définissent simplement des variables avant la prochaine partie de la chaîne). Puisque vous ne pouvez que flatMap
autres Future
, chaque instruction c0
, c1
, ... sauf que le dernier a intérêt à produire un Future
.
Maintenant, getUserDetails
et getSchool
produisent tous deux Futures
, mais sid
est un Option
, donc nous ne pouvons pas le mettre à droite -côté d'un <-
. Malheureusement, il n'y a aucun moyen propre et original de le faire. Si o
est une option, nous pouvons
o.map(Future.successful).getOrElse(Future.failed(new Exception))
pour transformer un Option
en Future
déjà terminé. Alors
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
fera l'affaire. Est-ce mieux que ce que vous avez? Douteux. Mais si tu
implicit class OptionIsFuture[A](val option: Option[A]) extends AnyVal {
def future = option.map(Future.successful).getOrElse(Future.failed(new Exception))
}
puis tout à coup la for-comprehension semble à nouveau raisonnable:
for {
ud <- userStore.getUserDetails(user.userId)
sid <- ud.right.toOption.flatMap(_.schoolId).future
s <- schoolStore.getSchool(sid)
} yield s
Est-ce la meilleure façon d'écrire ce code? Probablement pas; il repose sur la conversion d'un None
en exception simplement parce que vous ne savez pas quoi faire d'autre à ce stade. C'est difficile à contourner en raison des décisions de conception de Future
; Je dirais que votre code d'origine (qui appelle un filtre) est au moins aussi bon moyen de le faire.
Quel comportement aimeriez-vous se produire dans le cas où le Option[School]
est None
? Souhaitez-vous que le futur échoue? Avec quel genre d'exception? Souhaitez-vous qu'il ne soit jamais terminé? (Cela ressemble à une mauvaise idée).
Quoi qu'il en soit, la clause if
dans un desugars for-expression à un appel à la méthode filter
. Le contrat sur Future#filter
est ainsi:
Si l'avenir actuel contient une valeur qui satisfait le prédicat, le nouvel avenir conservera également cette valeur. Sinon, l'avenir résultant échouera avec une NoSuchElementException.
Mais attendez:
scala> None.get
Java.util.NoSuchElementException: None.get
Comme vous pouvez le voir, None.get renvoie exactement la même chose.
Ainsi, se débarrasser du if sid.isDefined
devrait fonctionner, et cela devrait donner un résultat raisonnable:
val schoolFuture = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- schoolStore.getSchool(sid.get)
} yield s
Gardez à l'esprit que le résultat de schoolFuture
peut être une instance de scala.util.Failure[NoSuchElementException]
. Mais vous n'avez pas décrit quel autre comportement vous aimeriez.
Nous avons fait un petit wrapper sur Future [Option [T]] qui agit comme une monade (personne n'a même vérifié aucune des lois de la monade, mais il y a map, flatMap, foreach, filter et ainsi de suite) - MaybeLater =. Il se comporte bien plus qu'une option asynchrone.
Il y a beaucoup de code malodorant, mais peut-être que ce sera utile au moins à titre d'exemple. BTW: il y a beaucoup de questions ouvertes ( ici par ex.)
C'est plus facile à utiliser https://github.com/qifun/stateless-future
ou https://github.com/scala/async
faire A-Normal-Form
transformer.