Фьючерсы - карта и карта
Я прочитал документы о map
и flatMap
, и я понимаю, что flatMap
используется для операции, которая принимает параметр Future
и возвращает другой Future
. Я не совсем понимаю, почему я хотел бы это сделать. Возьмите этот пример:
- Пользователь обращается к моему веб-сервису с просьбой "делать что-то"
- Я загружаю файл (который работает медленно)
- Я обрабатываю файл (который интенсивно работает с ЦП)
- Получить результат
Я понимаю, что хочу использовать будущее для загрузки файла, но у меня есть два варианта его обработки:
val downloadFuture = Future { downloadFile }
val processFuture = downloadFuture map { processFile }
processFuture onSuccess { case r => renderResult(r) }
или
val downloadFuture = Future { // download the file }
val processFuture = downloadFuture flatMap { Future { processFile } }
processFuture onSuccess { case r => renderResult(r) }
Добавляя операторы отладки (Thread.currentThread().getId
), я вижу, что в обоих случаях загрузка, process
и render
происходит в одном потоке (используя ExecutionContext.Implicits.global
).
Я использовал бы flatMap
просто для разделения downloadFile
и processFile
и убедитесь, что processFile
всегда работает в Future
, даже если он не был отображен из downloadFile
?
Ответы
Ответ 1
убедитесь, что processFile
всегда работает в Future
, даже если он не был отображен из downloadFile
?
Да, это правильно.
Однако в большинстве случаев вы не будете использовать Future { ... }
напрямую, вы должны использовать функции (из других библиотек или своих собственных), которые возвращают Future
.
Представьте себе следующие функции:
def getFileNameFromDB{id: Int) : Future[String] = ???
def downloadFile(fileName: String) : Future[java.io.File] = ???
def processFile(file: java.io.File) : Future[ProcessResult] = ???
Вы можете использовать flatMap
для их объединения:
val futResult: Future[ProcessResult] =
getFileNameFromDB(1).flatMap( name =>
downloadFile(name).flatMap( file =>
processFile(file)
)
)
Или используя для понимания:
val futResult: Future[ProcessResult] =
for {
name <- getFileNameFromDB(1)
file <- downloadFile(name)
result <- processFile(file)
} yield result
В большинстве случаев вы не вызываете onSuccess
(или onComplete
). Используя одну из этих функций, вы регистрируете функцию обратного вызова, которая будет выполняться, когда заканчивается Future
.
Если в нашем примере вы хотите отобразить результат обработки файла, вы вернете что-то вроде Future[Result]
вместо вызова futResult.onSuccess(renderResult)
. В последнем случае ваш тип возвращаемого значения будет Unit
, поэтому вы ничего не сможете вернуть.
В Play Framework это может выглядеть так:
def giveMeAFile(id: Int) = Action.async {
for {
name <- getFileNameFromDB(1)
file <- downloadFile(name)
processed <- processFile(file)
} yield Ok(processed.byteArray).as(processed.mimeType))
}
Ответ 2
Если у вас есть будущее, скажем, Future[HttpResponse]
, и вы хотите указать, что делать с этим результатом, когда он будет готов, например написать тело в файл, вы можете сделать что-то вроде responseF.map(response => write(response.body)
. Однако если write
также является асинхронным методом, который возвращает будущее, этот вызов map
возвращает тип типа Future[Future[Result]]
.
В следующем коде:
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
имеет тип Future[Future[String]]
, а flatStringF
имеет тип Future[String]
. Большинство согласится, второе - более полезно. Плоская карта поэтому полезна для составления нескольких фьючерсов вместе.
При использовании for
понятий с фьючерсами под капотом flatMap
используется вместе с 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)
Этот код даст 60.
Под капотом scala переводит это на
val resultF = threeF.flatMap(three => fourF.flatMap(four => fiveF.map(five => three * four * five)))
Ответ 3
def flatMap[B](f: A => Option[B]): Option[B] =
this match {
case None => None
case Some(a) => f(a)
}
Это простой пример того, как FlatMap работает для Option, это может помочь лучше понять. Это на самом деле состоит в том, что он не добавляет обертку снова. Что нам нужно.