期货 - 地图与平面图

Futures - map vs flatmap

我已经阅读了有关 mapflatMap 的文档,我知道 flatMap 用于接受 Future 参数和 returns 另一个 Future。我不完全理解的是为什么我要这样做。举个例子:

  1. 用户点击我的网络服务请求 "do stuff"
  2. 我下载一个文件(很慢)
  3. 我处理文件(CPU 密集)
  4. 渲染结果

我知道我想使用 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) 我看到在这两种下载情况下,processrender 发生在同一个线程中(使用 ExecutionContext.Implicits.global)。

我会使用 flatMap 来简单地解耦 downloadFileprocessFile 并确保 processFile 始终在 Future 中运行,即使它没有被映射来自 downloadFile?

ensure that processFile always runs in a Future even if it was not mapped from downloadFile?

是的,没错。

但是大多数时候你不会直接使用 Future { ... },你会使用函数(来自其他库或你自己的)return a 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)
    )
  )

或使用 for comprehension :

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

大多数时候你不会调用onSuccess(或onComplete)。通过使用这些函数之一,您可以注册一个回调函数,该函数将在 Future 完成时执行。

如果在我们的示例中您想要呈现文件处理的结果,您将 return 类似于 Future[Result] 而不是调用 futResult.onSuccess(renderResult)。在最后一种情况下,您的 return 类型将是 Unit,因此您不能真正 return 某些东西。

在 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))
}

如果你有一个未来,比方说,Future[HttpResponse],你想指定在结果准备好时如何处理它,比如将正文写入文件,你可以做类似的事情responseF.map(response => write(response.body)。然而,如果 write 也是一个异步方法,return 是一个未来,这个 map 调用将 return 像 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))

stringFFuture[Future[String]] 类型,而 flatStringFFuture[String] 类型。大多数人会同意,第二个更有用。因此,Flat Map 对于将多个 future 组合在一起很有用。

当您对 Futures 使用 for 理解时,在后台 flatMapmap 一起使用。

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)))
def flatMap[B](f: A => Option[B]): Option[B] = 
  this match {
    case None => None
    case Some(a) => f(a)
  }

这是一个简单的例子,说明了 flatMap 如何为 Option 工作,这有助于更好地理解,它实际上是在组合它而不是添加我们需要的包装器again.That。

转换函数的失败可能性是 Future 存在 flatMap 的另一个原因。

说你有一个 f: Future[T]。和一个转换 func: T => B。然而,由于某种原因,这个功能可能会失败。所以我们想指示调用者它失败了。

Future.map 如何实现这一点并不明显。但是 flatMap 你可以。因为使用 flatMap 它将 Future 视为 return,然后您可以轻松地执行 Future.failed(e) 以将错误冒泡给调用者。或者,如果成功,您可以使用 Future.success(r) 来 return 结果。

又名。把 func 变成 func: T => Future[B]

当您将操作与 Future 链接在一起并且操作可能在中间失败时,这非常有用。