J'ai lu les documents sur map
et flatMap
et je comprends que flatMap
est utilisé pour une opération qui accepte un paramètre Future
et renvoie un autre Future
. Ce que je ne comprends pas bien, c'est pourquoi je voudrais faire ça. Prenez cet exemple:
Je comprends que je voudrais utiliser un futur pour télécharger le fichier mais j'ai deux options pour le traiter:
val downloadFuture = Future { downloadFile }
val processFuture = downloadFuture map { processFile }
processFuture onSuccess { case r => renderResult(r) }
ou
val downloadFuture = Future { // download the file }
val processFuture = downloadFuture flatMap { Future { processFile } }
processFuture onSuccess { case r => renderResult(r) }
En ajoutant des instructions de débogage (Thread.currentThread().getId
), je vois que dans les deux cas, le téléchargement, process
et render
se produisent dans le même thread (en utilisant ExecutionContext.Implicits.global
).
Dois-je utiliser flatMap
simplement pour découpler downloadFile
et processFile
et m'assurer que processFile
s'exécute toujours dans un Future
même s'il n'est pas mappé de downloadFile
?
assurez-vous que
processFile
s'exécute toujours dans unFuture
même s'il n'a pas été mappé à partir dedownloadFile
?
Oui c'est correct.
Cependant, la plupart du temps, vous n'utiliseriez pas directement Future { ... }
, Vous utiliseriez des fonctions (provenant d'autres bibliothèques ou des vôtres) qui renvoient un Future
.
Imaginez les fonctions suivantes:
def getFileNameFromDB{id: Int) : Future[String] = ???
def downloadFile(fileName: String) : Future[Java.io.File] = ???
def processFile(file: Java.io.File) : Future[ProcessResult] = ???
Vous pouvez utiliser flatMap
pour les combiner:
val futResult: Future[ProcessResult] =
getFileNameFromDB(1).flatMap( name =>
downloadFile(name).flatMap( file =>
processFile(file)
)
)
Ou en utilisant un pour la compréhension:
val futResult: Future[ProcessResult] =
for {
name <- getFileNameFromDB(1)
file <- downloadFile(name)
result <- processFile(file)
} yield result
La plupart du temps, vous n'appelez pas onSuccess
(ou onComplete
). En utilisant l'une de ces fonctions, vous enregistrez une fonction de rappel qui sera exécutée lorsque le Future
se terminera.
Si, dans notre exemple, vous souhaitez afficher le résultat du traitement de fichier, vous devez renvoyer quelque chose comme Future[Result]
Au lieu d'appeler futResult.onSuccess(renderResult)
. Dans le dernier cas, votre type de retour serait Unit
, vous ne pouvez donc pas vraiment renvoyer quelque chose.
Dans Play Framework, cela pourrait ressembler à ceci:
def giveMeAFile(id: Int) = Action.async {
for {
name <- getFileNameFromDB(1)
file <- downloadFile(name)
processed <- processFile(file)
} yield Ok(processed.byteArray).as(processed.mimeType))
}
Si vous avez un avenir, disons, Future[HttpResponse]
, Et que vous souhaitez spécifier quoi faire avec ce résultat lorsqu'il est prêt, comme écrire le corps dans un fichier, vous pouvez faire quelque chose comme responseF.map(response => write(response.body)
. Cependant, si write
est également une méthode asynchrone qui renvoie un avenir, cet appel map
renverra un type comme Future[Future[Result]]
.
Dans le code suivant:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val numF = Future{ 3 }
val stringF = numF.map(n => Future(n.toString))
val flatStringF = numF.flatMap(n => Future(n.toString))
stringF
est de type Future[Future[String]]
tandis que flatStringF
est de type Future[String]
. La plupart seraient d'accord, le second est plus utile. Flat Map est donc utile pour composer plusieurs futurs ensemble.
Lorsque vous utilisez des compréhensions for
avec Futures, sous le capot flatMap
est utilisé avec map
.
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
val threeF = Future(3)
val fourF = Future(4)
val fiveF = Future(5)
val resultF = for{
three <- threeF
four <- fourF
five <- fiveF
}yield{
three * four * five
}
Await.result(resultF, 3 seconds)
Ce code donnera 60.
Sous le capot, scala traduit cela en
val resultF = threeF.flatMap(three => fourF.flatMap(four => fiveF.map(five => three * four * five)))
def flatMap[B](f: A => Option[B]): Option[B] =
this match {
case None => None
case Some(a) => f(a)
}
Ceci est un exemple simple où le flatMap fonctionne pour Option, cela peut aider à mieux comprendre, En fait, il ne compose pas l'ajout d'un wrapper. C'est ce dont nous avons besoin.