34

假设我有

val foo : Seq[Double] = ...
val bar : Seq[Double] = ...

我希望生成一个序列,其中 baz(i) = foo(i) + bar(i)。我能想到的一种方法是

val baz : Seq[Double] = (foo.toList zip bar.toList) map ((f: Double, b : Double) => f+b)

然而,这感觉既丑陋又低效——我必须将两个 seq 都转换为列表(它会随着惰性列表爆炸),创建这个临时的元组列表,只是为了映射它并让它被 GCed。也许流解决了懒惰的问题,但无论如何,这感觉都太丑陋了。在 lisp 中,map 函数将映射到多个序列。我会写

(mapcar (lambda (f b) (+ f b)) foo bar)

并且不会在任何地方创建临时列表。在 Scala 中是否有 map-over-multiple-lists 函数,或者 zip 与解构相结合真的是“正确”的方法吗?

4

7 回答 7

84

在 Scala 2.8 中:

val baz = (foo, bar).zipped map (_ + _)

它以相同的方式适用于两个以上的操作数。即你可以跟进这个:

(foo, bar, baz).zipped map (_ * _ * _)
于 2010-06-18T14:12:36.480 回答
15

您想要的函数被调用zipWith,但它不是标准库的一部分。它将在 2.8 中(更新:显然不是,请参阅评论)。

foo zipWith((f: Double, b : Double) => f+b) bar

请参阅这张 Trac 票

于 2009-07-21T07:14:24.960 回答
10

好吧,缺少 zip 是 Scala 的 2.7 Seq 的一个缺陷。Scala 2.8 有一个经过深思熟虑的集合设计,以取代 2.7 中出现的集合的特殊方式(请注意,它们并不是一次创建的,而是采用统一的设计)。

现在,当您想避免创建临时集合时,您应该在 Scala 2.7 上使用“投影”,或者在 Scala 2.8 上使用“视图”。这将为您提供一个集合类型,其中某些指令,特别是 map、flatMap 和 filter,是非严格的。在 Scala 2.7 上,List 的投影是 Stream。在 Scala 2.8 上,有一个序列的序列视图,但序列中有一个 zipWith,你甚至不需要它。

话虽如此,如前所述,JVM 针对处理临时对象分配进行了优化,并且在服务器模式下运行时,运行时优化可以创造奇迹。所以,不要过早优化。在将要运行的条件下测试代码——如果您还没有计划在服务器模式下运行它,那么请重新考虑代码是否预计会长时间运行,并优化何时/何地/如有必要。

编辑

Scala 2.8 上实际可用的是:

(foo,bar).zipped.map(_+_)
于 2009-07-21T11:59:30.560 回答
4

惰性列表不是列表的副本 - 它更像是单个对象。在惰性 zip 实现的情况下,每次请求下一个项目时,它都会从两个输入列表中的每一个中获取一个项目并从中创建一个元组,然后使用模式匹配将元组分开你的拉姆达。

因此,在开始对它们进行操作之前,无需创建整个输入列表的完整副本。它归结为与在 JVM 上运行的任何应用程序非常相似的分配模式——许多非常短暂但很小的分配,JVM 已对其进行了优化处理。

更新:要清楚,您需要使用 Streams(惰性列表)而不是列表。Scala 的流有一个以惰性方式工作的 zip,因此您不应该将内容转换为列表。

理想情况下,您的算法应该能够处理两个无限流而不会崩溃(当然,假设它不执行任何操作folding,而只是读取并生成流)。

于 2009-07-21T06:37:44.360 回答
1

当面临类似的任务时,我在Iterables 中添加了以下 pimp:

implicit class IterableOfIterablePimps[T](collOfColls: Iterable[Iterable[T]]) {
  def mapZipped[V](f: Iterable[T] => V): Iterable[V] = new Iterable[V] {
    override def iterator: Iterator[V] = new Iterator[V] {
      override def next(): V = {
        val v = f(itemsLeft.map(_.head))
        itemsLeft = itemsLeft.map(_.tail)
        v
      }

      override def hasNext: Boolean = itemsLeft.exists(_.nonEmpty)

      private var itemsLeft = collOfColls
    }
  }
}

有了这个,一个人可以做类似的事情:

val collOfColls = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))
collOfColls.mapZipped { group =>
  group // List(1, 4, 7), then List(2, 5, 8), then List(3, 6, 9)
}

请注意,您应该仔细考虑作为嵌套传递的集合类型Iterable,因为tail并且head将在它上面重复调用。因此,理想情况下,您应该使用 fastIterable[List]和.tailhead

此外,此代码需要相同大小的嵌套集合。那是我的用例,但如果需要,我怀疑这可以改进。

于 2015-08-12T20:40:19.343 回答
1

从 Scala 2.13.0 开始

.zipped已弃用,取而代之的是lazyZip。示例代码:

val mangoes = List(1, 2, 3, 4, 5)
val oranges = List(2, 3, 4, 5, 6)
val fruits = (mangoes lazyZip oranges).map((m, o) => m+o)
print(fruits)

印刷

List(3, 5, 7, 9, 11)
于 2021-01-25T18:01:16.477 回答
0

更新:有人指出(在评论中)这个“答案”实际上并没有解决被问到的问题。这个答案将映射 和 的每个组合产生N x M元素,而不是要求的min(M, N)。所以,这是错误的,但留给后代,因为它是很好的信息。foobar


最好的方法是flatMap结合 with map。代码胜于雄辩:

foo flatMap { f => bar map { b => f + b } }

Seq[Double]正如您所期望的那样,这将产生一个单一的。这种模式非常普遍,以至于 Scala 实际上包含了一些实现它的语法魔法:

for {
  f <- foo
  b <- bar
} yield f + b

或者,或者:

for (f <- foo; b <- bar) yield f + b

for { ... }语法确实是最惯用的方法。您可以根据需要继续添加生成器子句(例如b <- bar)。因此,如果它突然变成您必须映射的三个 Seqs,您可以轻松地根据您的要求扩展您的语法(创造一个短语)。

于 2009-07-21T13:41:41.280 回答