web-dev-qa-db-fra.com

Futures - carte vs flatmap

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:

  1. L'utilisateur accède à mon service Web pour lui demander de "faire des choses"
  2. Je télécharge un fichier (qui est lent)
  3. Je traite le fichier (qui est gourmand en CPU)
  4. Rendre le résultat

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?

23
user404345

assurez-vous que processFile s'exécute toujours dans un Future même s'il n'a pas été mappé à partir de downloadFile?

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))
}
24
Peter Neyens

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

1
Dhiraj Himani