2015-07-26 37 views
10

Czytałem dokumenty dotyczące map i flatMap i rozumiem, że flatMap służy do operacji, która przyjmuje parametr Future i powraca kolejny Future. Nie do końca rozumiem, dlaczego chciałbym to zrobić. Weźmy ten przykład:Futures - mapa vs flatmap

  1. użytkownika uderza mój usługa pytaniem „robić rzeczy”
  2. pobrać plik (który jest wolny)
  3. przetwarzać plik (który obciąża CPU)
  4. renderować wynik

rozumiem, że chciałbym skorzystać z przyszłości, aby pobrać plik, ale muszę mieć dwie możliwości ponownego przetworzenia go:

val downloadFuture = Future { downloadFile } 
val processFuture = downloadFuture map { processFile } 
processFuture onSuccess { case r => renderResult(r) } 

lub

val downloadFuture = Future { // download the file } 
val processFuture = downloadFuture flatMap { Future { processFile } } 
processFuture onSuccess { case r => renderResult(r) } 

Dodając oświadczenia debugowania (Thread.currentThread().getId) Widzę, że w obu przypadkach pobranie, process i render występować w tym samym wątku (używając ExecutionContext.Implicits.global).

użyłbym flatMap prostu oddzielić downloadFile i processFile i upewnić się, że processFile zawsze działa w Future nawet jeśli nie został odwzorowany z downloadFile?

Odpowiedz

14

zapewnić processFile zawsze działa w Future nawet jeśli nie został odwzorowany z downloadFile?

Tak, zgadza się.

Jednak w większości przypadków nie używałbyś bezpośrednio nazwy Future { ... }, używałbyś funkcji (z innych bibliotek lub własnych), które zwracają Future.

Wyobraźmy sobie następujące funkcje:

def getFileNameFromDB{id: Int) : Future[String] = ??? 
def downloadFile(fileName: String) : Future[java.io.File] = ??? 
def processFile(file: java.io.File) : Future[ProcessResult] = ??? 

Można użyć flatMap je łączyć:

val futResult: Future[ProcessResult] = 
    getFileNameFromDB(1).flatMap(name => 
    downloadFile(name).flatMap(file => 
     processFile(file) 
    ) 
) 

lub używanie do zrozumienia:

val futResult: Future[ProcessResult] = 
    for { 
    name <- getFileNameFromDB(1) 
    file <- downloadFile(name) 
    result <- processFile(file) 
    } yield result 

Większość czasu byś nie dzwoń pod numer onSuccess (lub onComplete). Korzystając z jednej z tych funkcji, rejestrujesz funkcję zwrotną, która zostanie wykonana, gdy zakończy się Future.

Jeśli w naszym przykładzie chciałbyś wyrenderować wynik przetwarzania pliku, zwrócisz coś w rodzaju Future[Result] zamiast wywoływać futResult.onSuccess(renderResult). W ostatnim przypadku Twój typ zwrotu to: Unit, więc nie możesz czegoś zwrócić.

In Play Framework może to wyglądać tak:

def giveMeAFile(id: Int) = Action.async { 
    for { 
    name <- getFileNameFromDB(1) 
    file <- downloadFile(name) 
    processed <- processFile(file) 
    } yield Ok(processed.byteArray).as(processed.mimeType)) 
} 
+0

Dzięki za potwierdzenie tego peter –

12

Jeśli masz przed sobą przyszłość, powiedzmy, Future[HttpResponse] i chcesz określić, co robić z tego wyniku, gdy jest gotowy, jak napisać do pliku, możesz zrobić coś takiego, jak responseF.map(response => write(response.body). Jeśli jednak write jest również metodą asynchroniczną, która zwraca przyszłość, to wywołanie map zwróci typ taki jak Future[Future[Result]].

W poniższym kodzie:

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 jest typu Future[Future[String]] podczas flatStringF jest typu Future[String]. Większość zgodziłaby się, druga jest bardziej przydatna. Płaska mapa jest zatem przydatna do wspólnego tworzenia wielu kontraktów futures.

Podczas korzystania ze sprawdzeń z Futures w wersjach for, pod maską flatMap używana jest razem z 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) 

Kod ten przyniesie 60.

Pod maską scala przekłada się to na

val resultF = threeF.flatMap(three => fourF.flatMap(four => fiveF.map(five => three * four * five))) 
0
def flatMap[B](f: A => Option[B]): Option[B] = 
    this match { 
    case None => None 
    case Some(a) => f(a) 
    } 

Jest to prosty przykład, w którym jak flatMap pracuje dla opcji, może to Pomóżcie lepiej zrozumieć, to właściwie komponowanie to nie jest ponowne dodawanie opakowania. Tego właśnie potrzebujemy.