无法在Future中使用for循环来映射List

43

我每次都需要绕过一个问题。我无法使用for推导式对包含在Future中的内容进行映射。

例如:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

val f = Future( List("A", "B", "C") )
for {
  list <- f
  e <- list
} yield (e -> 1)

这给了我一个错误:

 error: type mismatch;
 found   : List[(String, Int)]
 required: scala.concurrent.Future[?]
              e <- list
                ^

但是如果我这样做,它可以正常工作:

f.map( _.map( (_ -> 1) ) )

如果我不能使用for推导来做这件事,那么在我的另一个例子中它能够工作的原因是我没有使用flatmap吗?我正在使用Scala 2.10.0。


1
由于这个问题在过去几年中引起了一些关注,我想补充一下,如果你想进一步研究,单子变换器是正确的搜索术语。今天,我会采取一些不同的方法,以避免出现这种情况。 - Magnus
4个回答

67

当您在单个for循环中使用多个生成器时,您正在展开结果类型。也就是说,您不会得到一个List[List[T]],而是得到一个List[T]

scala> val list = List(1, 2, 3)
list: List[Int] = List(1, 2, 3)

scala> for (a <- list) yield for (b <- list) yield (a, b)
res0: List[List[(Int, Int)]] = List(List((1,1), (1,2), (1,3)), List((2,1
), (2,2), (2,3)), List((3,1), (3,2), (3,3)))

scala> for (a <- list; b <- list) yield (a, b)
res1: List[(Int, Int)] = List((1,1), (1,2), (1,3), (2,1), (2,2), (2,3),
(3,1), (3,2), (3,3))

现在,你要如何转化一个 Future[List[T]] ?它不能是一个 Future[T],因为你将会得到多个 T,而一个 Future(与 List 不同)只能存储其中一个。顺便说一下,Option 也存在同样的问题:
scala> for (a <- Some(3); b <- list) yield (a, b)
<console>:9: error: type mismatch;
 found   : List[(Int, Int)]
 required: Option[?]
              for (a <- Some(3); b <- list) yield (a, b)
                                   ^

最简单的方法是将多个for循环嵌套在一起来完成操作:
scala> for {
     |   list <- f
     | } yield for {
     |   e <- list
     | } yield (e -> 1)
res3: scala.concurrent.Future[List[(String, Int)]] = scala.concurrent.im
pl.Promise$DefaultPromise@4f498585

回顾起来,这个限制应该是相当明显的。问题在于,几乎所有的例子都使用集合,而所有的集合只是 GenTraversableOnce ,因此它们可以自由地混合在一起。此外,Scala被批评的 CanBuildFrom 机制使得可以混入任意集合并返回特定类型,而不是 GenTraversableOnce。
更让事情变得模糊的是,Option 可以转换为 Iterable,这使得可以将选项与集合组合在一起,只要选项不是第一个。
但是,在我看来,造成困惑的主要原因是,在教学推导时从来没有提到这个限制。

1
不错的回答,但我认为第二个代码块存在一些复制/粘贴问题:它与第一个相同。;-) - Hiura
这个值得两次点赞…不管怎样,我想知道人们认为哪个更清晰:map 版本还是 for yield for 嵌套版本? - Mortimer
@Mortimer 没有统一的共识,实际上许多人会根据情况选择使用两种方式。 - Daniel C. Sobral

8

嗯,我想我明白了。我需要将其包装在Future中,因为for推导式会添加flatmap。

这样做是有效的:

for {
  list <- f
  e <- Future( list )
} yield (e -> 1)

当我添加上述内容时,我还没有看到任何答案。但是,可以在一个for-comprehension内完成工作。不确定是否值得使用Future来降低开销(通过使用successful应该没有开销)。

for {
  list1 <- f
  list2 <- Future.successful( list1.map( _ -> 1) )
  list3 <- Future.successful( list2.filter( _._2 == 1 ) )
} yield list3

半年后的附加说明。

解决此问题的另一种方法是在拥有与初始映射返回类型不同的其他类型时,直接使用赋值=而不是<-

使用赋值时,该行不会被平铺。您现在可以自由地进行显式映射(或其他转换),以返回不同类型。

如果您有几个转换步骤,其中一个步骤的返回类型与其他步骤不同,但仍希望使用for-comprehension语法,因为它使您的代码更易读,则此方法非常有用。

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

val f = Future( List("A", "B", "C") )
def longRunning( l:List[(String, Int)] ) = Future.successful( l.map(_._2) )

for {
  list <- f
  e = list.map( _ -> 1 )
  s <- longRunning( e )
} yield s

在for循环中进行赋值,看起来确实是正确的解决方案。 - khebbie

5

你原始的版本无法编译,因为ListFuture是不同的单子。为了解释为什么这是一个问题,请考虑它的展开形式:

f.flatMap(list => list.map(e => e -> 1))

显然,list.map(_ -> 1)是一个由(String, Int)对组成的列表,因此我们需要将其作为参数传递给flatMap函数,该函数将字符串列表映射到这些对组成的列表。但我们需要将字符串列表映射到某种类型的Future。因此,这段代码无法编译。
您所提供的版本可以编译,但它并不能达到您想要的效果。它会被转换成以下代码形式:
f.flatMap(list => Future(list).map(e => e -> 1))

这次类型对齐了,但我们并没有做什么有趣的事情——我们只是从Future中提取出值,将其放回到一个Future中,并映射结果。因此,我们最终得到的是Future[(List[String], Int)]类型的东西,而我们想要的是Future[List[(String, Int)]]

你所做的是一种双重映射操作,涉及两个(不同的)嵌套单子,这不是for理解式能帮助你解决的问题。幸运的是,f.map(_.map(_ -> 1))正好做到了你想要的,并且清晰简洁。

5
我知道“map”的意思,但当我看到它在一行文字中出现两次并且有两种不同的意思时,就不那么清楚了,我必须停下来思考一下。我并不介意思考,但当我停下来时,就有可能走向冰箱。 - som-snytt
我必须同意som-snytt的观点,在这种情况下,我进行了三到四次转换。因此,我想避免深度嵌套的映射。在撰写这个问题之前,我已经将那些映射步骤提取到函数中,然后以嵌套的第三(第二(第一(1)))的方式调用它们。但是那样读起来很反常。我喜欢for-comps,因为你可以阅读它们并跟随转换流程而不会有太多噪音。正如你所说,在这种情况下,for-comp的工作原理并不是真正的(除非你使用scalaz,其中函数也成为monads?)。 - Magnus

1

我认为这个表格比串行映射或串行产量更易读:

for (vs <- future(data);
     xs = for (x <- vs) yield g(x)
) yield xs

以元组映射为代价:

f.map((_, xs)).map(_._2)

更准确地说:

f.map((vs: List[Int]) => (vs, for (x <- vs) yield g(x))).map(_._2)

当然,这段代码只是为了解答问题而提供的示例。我最初想做的是在 Future 中进行一些 IO 操作,然后解析结果并将其包装在一个 case class 中,最后将其传输到另一个 actor。我已经将这些步骤分别定义为 def,并希望以简洁的方式应用它们。 - Magnus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接