web-dev-qa-db-fra.com

Future [Option] dans Scala for-comprehensions

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
}
36
Ryan Bair

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.

13
Ben James

(Modifié pour donner une réponse correcte!)

La clé ici est que Future et Optionne 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.

22
Rex Kerr

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#filterest 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.

8
stephenjudkins

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.)

3
Alex Povar

C'est plus facile à utiliser https://github.com/qifun/stateless-future ou https://github.com/scala/async faire A-Normal-Form transformer.

1
jilen