Scala: List [Future] to Future [List] sans tenir compte des futurs ratés

116

Je cherche un moyen de convertir une liste de longueur arbitraire de Futures en Future of List. J'utilise Playframework, donc en fin de compte, ce que je veux vraiment, c'est un Future[Result], mais pour simplifier les choses, disons simplement que Future[List[Int]]la façon normale de faire cela serait d'utiliser Future.sequence(...)mais il y a une torsion ... La liste que l'on me donne a généralement environ 10-20 futurs, et il n'est pas rare que l'un de ces futurs échoue (ils font des demandes de service Web externes). Au lieu d'avoir à les réessayer tous au cas où l'un d'eux échouerait, j'aimerais pouvoir accéder à ceux qui ont réussi et les renvoyer.

Par exemple, faire ce qui suit ne fonctionne pas

import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Success
import scala.util.Failure

val listOfFutures = Future.successful(1) :: Future.failed(new Exception("Failure")) :: 
                    Future.successful(3) :: Nil

val futureOfList = Future.sequence(listOfFutures)

futureOfList onComplete {
  case Success(x) => println("Success!!! " + x)
  case Failure(ex) => println("Failed !!! " + ex)
}

scala> Failed !!! java.lang.Exception: Failure

Au lieu d'obtenir la seule exception, j'aimerais pouvoir retirer les 1 et 3 de là. J'ai essayé d'utiliser Future.fold, mais cela n'appelle apparemment que Future.sequencedans les coulisses.

Merci d'avance pour l'aide!

Joe
la source

Réponses:

147

L'astuce consiste d'abord à s'assurer qu'aucun des futurs n'a échoué. .recoverest votre ami ici, vous pouvez le combiner avec mappour convertir tous les Future[T]résultats en Future[Try[T]]]instances, qui sont toutes sûres d'être des futurs réussis.

Remarque: vous pouvez également utiliser Optionou Eitherici, mais Tryc'est le moyen le plus propre si vous souhaitez spécifiquement intercepter les exceptions

def futureToFutureTry[T](f: Future[T]): Future[Try[T]] =
  f.map(Success(_)).recover { case x => Failure(x)}

val listOfFutures = ...
val listOfFutureTrys = listOfFutures.map(futureToFutureTry(_))

Puis utilisez Future.sequencecomme avant, pour vous donner unFuture[List[Try[T]]]

val futureListOfTrys = Future.sequence(listOfFutureTrys)

Puis filtrez:

val futureListOfSuccesses = futureListOfTrys.map(_.filter(_.isSuccess))

Vous pouvez même retirer les pannes spécifiques, si vous en avez besoin:

val futureListOfFailures = futureListOfTrys.map(_.filter(_.isFailure))
Kevin Wright
la source
Merci! .recoverétait en effet la pièce manquante pour moi.
Joe le
20
Vous pouvez utiliser _.collect{ case Success(x) => x}au lieu de _.filter(_.isSuccess)pour vous débarrasser de ce Trytype de fichier futureListOfSuccesses.
senia
43
Dans scala 2010 .recover(x => Failure(x))n'est pas valide, utilisez à la .recover({case e => Failure(e)})place
FGRibreau
Je pense que le futur wrapper vous manque: def futureToFutureOfTry [A] (f: Future [A]): ​​Future [Try [A]] = {val p = Promise [Try [A]] () f.map {a => p.success (scala.util.Success (a))} .recover {case x: Throwable => p.success (Failure (x))} p.future}
Dario
non. Je mappe un avenir à un autre avenir, une promesse intermédiaire n'est pas nécessaire et serait un gaspillage
Kevin Wright
12

Scala 2.12 a une amélioration Future.transformqui se prête à une réponse avec moins de codes.

val futures = Seq(Future{1},Future{throw new Exception})

// instead of `map` and `recover`, use `transform`
val seq = Future.sequence(futures.map(_.transform(Success(_)))) 

val successes = seq.map(_.collect{case Success(x)=>x})
successes
//res1: Future[Seq[Int]] = Future(Success(List(1)))

val failures = seq.map(_.collect{case Failure(x)=>x})
failures
//res2: Future[Seq[Throwable]] = Future(Success(List(java.lang.Exception)))
WeiChing 林 煒 清
la source
11

J'ai essayé la réponse de Kevin, et j'ai rencontré un problème sur ma version de Scala (2.11.5) ... J'ai corrigé cela, et j'ai écrit quelques tests supplémentaires si quelqu'un est intéressé ... voici ma version>

implicit class FutureCompanionOps(val f: Future.type) extends AnyVal {

    /** Given a list of futures `fs`, returns the future holding the list of Try's of the futures from `fs`.
      * The returned future is completed only once all of the futures in `fs` have been completed.
      */
    def allAsTrys[T](fItems: /* future items */ List[Future[T]]): Future[List[Try[T]]] = {
      val listOfFutureTrys: List[Future[Try[T]]] = fItems.map(futureToFutureTry)
      Future.sequence(listOfFutureTrys)
    }

    def futureToFutureTry[T](f: Future[T]): Future[Try[T]] = {
      f.map(Success(_)) .recover({case x => Failure(x)})
    }

    def allFailedAsTrys[T](fItems: /* future items */ List[Future[T]]): Future[List[Try[T]]] = {
      allAsTrys(fItems).map(_.filter(_.isFailure))
    }

    def allSucceededAsTrys[T](fItems: /* future items */ List[Future[T]]): Future[List[Try[T]]] = {
      allAsTrys(fItems).map(_.filter(_.isSuccess))
    }
}


// Tests... 



  // allAsTrys tests
  //
  test("futureToFutureTry returns Success if no exception") {
    val future =  Future.futureToFutureTry(Future{"mouse"})
    Thread.sleep(0, 100)
    val futureValue = future.value
    assert(futureValue == Some(Success(Success("mouse"))))
  }
  test("futureToFutureTry returns Failure if exception thrown") {
    val future =  Future.futureToFutureTry(Future{throw new IllegalStateException("bad news")})
    Thread.sleep(5)            // need to sleep a LOT longer to get Exception from failure case... interesting.....
    val futureValue = future.value

    assertResult(true) {
      futureValue match {
        case Some(Success(Failure(error: IllegalStateException)))  => true
      }
    }
  }
  test("Future.allAsTrys returns Nil given Nil list as input") {
    val future =  Future.allAsTrys(Nil)
    assert ( Await.result(future, 100 nanosecond).isEmpty )
  }
  test("Future.allAsTrys returns successful item even if preceded by failing item") {
    val future1 =  Future{throw new IllegalStateException("bad news")}
    var future2 = Future{"dog"}

    val futureListOfTrys =  Future.allAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys, 10 milli)
    System.out.println("successItem:" + listOfTrys);

    assert(listOfTrys(0).failed.get.getMessage.contains("bad news"))
    assert(listOfTrys(1) == Success("dog"))
  }
  test("Future.allAsTrys returns successful item even if followed by failing item") {
    var future1 = Future{"dog"}
    val future2 =  Future{throw new IllegalStateException("bad news")}

    val futureListOfTrys =  Future.allAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys,  10 milli)
    System.out.println("successItem:" + listOfTrys);

    assert(listOfTrys(1).failed.get.getMessage.contains("bad news"))
    assert(listOfTrys(0) == Success("dog"))
  }
  test("Future.allFailedAsTrys returns the failed item and only that item") {
    var future1 = Future{"dog"}
    val future2 =  Future{throw new IllegalStateException("bad news")}

    val futureListOfTrys =  Future.allFailedAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys,  10 milli)
    assert(listOfTrys(0).failed.get.getMessage.contains("bad news"))
    assert(listOfTrys.size == 1)
  }
  test("Future.allSucceededAsTrys returns the succeeded item and only that item") {
    var future1 = Future{"dog"}
    val future2 =  Future{throw new IllegalStateException("bad news")}

    val futureListOfTrys =  Future.allSucceededAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys,  10 milli)
    assert(listOfTrys(0) == Success("dog"))
    assert(listOfTrys.size == 1)
  }
Chris Bedford
la source
7

Je viens de tomber sur cette question et j'ai une autre solution à proposer:

def allSuccessful[A, M[X] <: TraversableOnce[X]](in: M[Future[A]])
                                                (implicit cbf: CanBuildFrom[M[Future[A]], A, M[A]], 
                                                 executor: ExecutionContext): Future[M[A]] = {
    in.foldLeft(Future.successful(cbf(in))) {
      (fr, fa)(for (r ← fr; a ← fa) yield r += a) fallbackTo fr
    } map (_.result())
}

L'idée ici est que dans le repli, vous attendez que l'élément suivant de la liste soit terminé (en utilisant la syntaxe pour la compréhension) et si le suivant échoue, vous revenez simplement à ce que vous avez déjà.

Idan Waisman
la source
Je n'aime pas le nom mais j'aime la façon dont c'est fait, directement de la séquence
impl
1

Vous pouvez facilement encapsuler le résultat futur avec l'option, puis aplatir la liste:

def futureToFutureOption[T](f: Future[T]): Future[Option[T]] =
    f.map(Some(_)).recover {
      case e => None
    }
val listOfFutureOptions = listOfFutures.map(futureToFutureOption(_))

val futureListOfOptions = Future.sequence(listOfFutureOptions)

val futureListOfSuccesses = futureListOfOptions.flatten
Amir Hossein Javan
la source
Juste au cas où quelqu'un d'autre rencontre une erreur avec Some dans la première fonction, la première fonction peut être réécrite comme ceci pour éviter une erreur du compilateur: def futureToFutureOption [T] (f: Future [T]): Future [Option [T]] = f.map (Option (_)). recover {case e => None}
Zee
0

Vous pouvez également collecter les résultats réussis et non réussis dans différentes listes:

def safeSequence[A](futures: List[Future[A]]): Future[(List[Throwable], List[A])] = {
  futures.foldLeft(Future.successful((List.empty[Throwable], List.empty[A]))) { (flist, future) =>
    flist.flatMap { case (elist, alist) =>
      future
        .map { success => (elist, alist :+ success) }
        .recover { case error: Throwable => (elist :+ error, alist) }
    }
  }
}
Evgeniy Lyutikov
la source