Scala: List [Future] to Future [List] ignorando i futuri falliti


116

Sto cercando un modo per convertire un elenco di future di lunghezza arbitraria in un futuro di elenco. Sto usando Playframework, quindi alla fine quello che voglio veramente è un fileFuture[Result] , ma per rendere le cose più semplici, diciamo solo che Future[List[Int]]il modo normale per farlo sarebbe usare Future.sequence(...)ma c'è una svolta ... L'elenco che mi viene dato di solito ha circa 10-20 futures in esso, e non è raro che uno di questi futures fallisca (stanno facendo richieste di servizi web esterni). Invece di dover riprovare tutti loro nel caso in cui uno di loro fallisca, mi piacerebbe essere in grado di ottenere quelli che sono riusciti e restituirli.

Ad esempio, fare quanto segue non funziona

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

Invece di ottenere l'unica eccezione, mi piacerebbe essere in grado di estrarre l'1 e il 3 da lì. Ho provato a usareFuture.fold , ma a quanto pare questo chiama solo Future.sequencedietro le quinte.

Grazie in anticipo per l'aiuto!

Risposte:


147

Il trucco è innanzitutto assicurarsi che nessuno dei futuri abbia fallito. .recoverè tuo amico qui, puoi combinarlo con mapper convertire tutti i Future[T]risultati in Future[Try[T]]]istanze, che sicuramente saranno futuri di successo.

nota: puoi usare Optiono Eitheranche qui, ma Tryè il modo più pulito se vuoi specificamente intercettare le eccezioni

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

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

Quindi usa Future.sequencecome prima, per darti un fileFuture[List[Try[T]]]

val futureListOfTrys = Future.sequence(listOfFutureTrys)

Quindi filtra:

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

Puoi persino estrarre i guasti specifici, se ne hai bisogno:

val futureListOfFailures = futureListOfTrys.map(_.filter(_.isFailure))

Grazie! .recoverera davvero il pezzo mancante per me.
Joe

20
Potresti usare _.collect{ case Success(x) => x}invece di _.filter(_.isSuccess)sbarazzarti di Tryin tipo di futureListOfSuccesses.
senia

43
In scala 2010 .recover(x => Failure(x))non è valida, utilizzare .recover({case e => Failure(e)})invece
FGRibreau

Penso che ti manchi il wrapper del futuro: 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 così. Sto mappando un futuro in un altro futuro, una promessa non è necessaria e sarebbe uno spreco
Kevin Wright

12

Scala 2.12 ha un miglioramento su Future.transformquello che si presta a una risposta con meno codici.

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

11

Ho provato la risposta di Kevin e mi sono imbattuto in un problema tecnico sulla mia versione di Scala (2.11.5) ... L'ho corretto e ho scritto alcuni test aggiuntivi se qualcuno è interessato ... ecco la mia versione>

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

7

Mi sono appena imbattuto in questa domanda e ho un'altra soluzione da offrire:

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'idea qui è che all'interno della piega stai aspettando il completamento dell'elemento successivo nell'elenco (usando la sintassi per la comprensione) e se il successivo fallisce, fai semplicemente il fallback a ciò che hai già.


Non mi piace il nome ma mi piace come è fatto, direttamente dalla sequenza
impl

1

Puoi facilmente avvolgere il risultato futuro con l'opzione e quindi appiattire l'elenco:

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

Nel caso in cui qualcun altro riscontri un errore con Some nella prima funzione, la prima funzione può essere riscritta in questo modo per evitare errori del compilatore: def futureToFutureOption [T] (f: Future [T]): Future [Option [T]] = f.map (Opzione (_)). recover {case e => None}
Zee

0

Puoi anche raccogliere risultati riusciti e non riusciti in diversi elenchi:

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) }
    }
  }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.