Xu Wenhao

View on GitHub
10 February 2014

Finagle里Future的flatMap

by Xu Wenhao

最近业余时间在学习Scala,最近的一个目标是用Finagle写一个简单和现有系统相关的查询服务。在看Finagle里面Future的flatMap的时候被绕了一下,所以简单做个记录。

flatMap是flatten和map两个函数的组合,先map,然后flatten,这个在collection类型的对象上很容易理解,这个对于写过Ruby或者Python这样的脚本语言的人应该很直观就能认识,好比下面的代码这样

val nestedNumbers = List(List(1, 2), List(3, 4))  
nestedNumbers.flatMap(x => x.map(_ * 2))

然后返回的结果就是这样

res0: List[Int] = List(2, 4, 6, 8)

这个代码相当于这样

val nestedNumbers = List(List(1,2), List(3,4))  
val mappedNestedNumbers = nestedNumber.map(x => x.map(_ * 2))  
val flatMappedNumber = mappedNestedNumbers.flatten

但是当直接用flatMap来完成Future的组合和链式调用的时候我一开始一直没有理解,如果把一个Future作为一个Collection,不应该map之后可以是任意东西,然后flatten之后就不是一个Future了,而为什么实际的代码示例是返回的还是一个Future呢?比如来自Finagle的Guide里面下面这样的示例呢?

def fetchUrl(url: String): Future[Array[Byte]]  
def findImageUrls(bytes: Array[Byte]): Seq[String]
val url = "http://www.google.com"
val f: Future[Array[Byte]] = fetchUrl(url) flatMap { bytes =>  
  val images = findImageUrls(bytes)  
  if (images.isEmpty)  
    Future.exception(new Exception("no image"))  
  else  
    fetchUrl(images(0))  
}
f onSuccess { image =>  
  println("Found image of size "+image.size)  
}
然后花了一点时间看了一下Effective Scala,发现之前理解错了。这个事情原来是这样的,首先,我们看一下Future的flatMap方法的函数签名和含义
def flatMap[B](f: A => Future[B]): Future[B] = {  
   val mapped: Future[Future[B]] = this map f  
   val flattened: Future[B] = mapped.flatten  
   flattened  
}
需要注意的是,实际Twitter Util库中flattenMap的实现不是这样的,这里只是为了说明语义。  
这个函数签名,也就是说,对于当前的Future容器内的对象A(这里只有一个A,而不是List这样有一个List的A),接收一个函数,输入是A,map成为Future[B],然后map函数本身再回把他重新封装回容器Future,那么就编程了Future[Future[B]]。  
然后flatten再去除外部的第一层容器,最终的返回值就变成了Future[B]。
这个解释了一个Future[A],以及一个针对A映射到Future[B]的函数g,通过flatMap组合之后,返回值就是第二个用于映射的函数返回的Future[B]。而Future[A]本身可以是一个函数f的计算结果。那么对于 f(g) 来说,就是两个函数通过flatMap组成了一个新函数。
这个机制在Twitter Util的Future以及2.10的官方库的Future的实现是类似的。
准备之后再研究一下Future和Future的实际flatMap的实现,看看这个异步Callback的机制是泽呢么实现的。
tags: