Фьючерсы - карта и карта

Я прочитал документы о 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, это может помочь лучше понять. Это на самом деле состоит в том, что он не добавляет обертку снова. Что нам нужно.