【问题标题】:Scala: Can there be any reason to prefer `filter+map` over `collect`?Scala:有什么理由更喜欢`filter+map`而不是`collect`?
【发布时间】:2016-05-01 00:38:58
【问题描述】:

有什么理由更喜欢filter+map

list.filter (i => aCondition(i)).map(i => fun(i))

超过collect? :

list.collect(case i if aCondition(i) => fun(i))

带有collect(单眼)的那个在我看来更快更干净。所以我会一直选择collect

【问题讨论】:

  • 我不确定性能会有多大不同。它们都遍历完整序列一次,并且仅将谓词函数应用于满足条件的项目,因此它们都在做相同的基本工作量。这实际上归结为编译器的智能程度、序列的大小以及执行条件和函数的成本。
  • 虽然我认为第二个可能会分配更少的序列,因为它不需要为filter 调用的返回而烦恼。
  • @IanMcLaird 我认为这是主要的争论。第二个分配的序列是否正确(因此效率更高)?
  • 原因与更喜欢map+flatten 而不是flatMap ...

标签: scala


【解决方案1】:

Scala 的大多数集合都急切地应用操作,并且(除非您使用的是为您执行此操作的宏库)不会融合操作。所以filter 后跟map 通常会创建两个集合(即使你使用Iterator 或类似的东西,中间形式也会暂时创建,尽管一次只创建一个元素),而collect 不会。

另一方面,collect 使用偏函数来实现联合测试,偏函数在测试集合中是否存在某些东西时比谓词 (A => Boolean) 慢。

此外,在某些情况下,读取一个比另一个更清晰,并且您不关心性能或内存使用差异 2 倍左右。在这种情况下,使用更清晰的那个。一般来说,如果你已经命名了函数,阅读起来会更清晰

xs.filter(p).map(f)
xs.collect{ case x if p(x) => f(x) }

但如果您内联提供闭包,collect 通常看起来更干净

xs.filter(x < foo(x, x)).map(x => bar(x, x))
xs.collect{ case x if foo(x, x) => bar(x, x) }

即使它不一定更短,因为您只引用了一次变量。

现在,性能差异有多大?这会有所不同,但如果我们考虑这样的集合:

val v = Vector.tabulate(10000)(i => ((i%100).toString, (i%7).toString))

并且你想根据过滤第一个条目来挑选第二个条目(所以过滤和映射操作都很容易),那么我们得到下表。

注意:可以将惰性视图放入集合中并在那里收集操作。您并不总能找回原来的类型,但您始终可以使用 to 获得正确的集合类型。因此,xs.view.filter(p).map(f).toVector 会因为视图而不会创建中间体。这也在下面进行了测试。也有人建议可以xs.flatMap(x =&gt; if (p(x)) Some(f(x)) else None),这是高效事实并非如此。下面也进行了测试。并且可以通过显式创建一个构建器来避免偏函数:val vb = Vector.newBuilder[String]; xs.foreach(x =&gt; if (p(x)) vb += f(x)); vb.result,其结果也在下面列出。

在下表中,测试了三个条件:什么都不过滤、过滤一半、什么都过滤。时间已标准化为过滤器/映射(100% = 与过滤器/映射相同的时间,越低越好)。误差范围约为 +- 3%。

不同过滤器/映射替代方案的性能

====================== Vector ========================
filter/map   collect  view filt/map  flatMap   builder
   100%        44%          64%        440%      30%    filter out none
   100%        60%          76%        605%      42%    filter out half
   100%       112%         103%       1300%      74%    filter out all

因此,filter/mapcollect 通常非常接近(当您保留很多时,collect 获胜),flatMap 在所有情况下都慢得多,并且创建构建器总是获胜。 (Vector 尤其如此。其他集合可能有一些不同的特征,但大多数的趋势会相似,因为操作上的差异是相似的。)this 中的视图em> 测试往往是一个胜利,但它们并不总是无缝地工作(而且它们并不比collect 更好,除了空的情况)。

所以,底线:如果在速度无关紧要时有助于清晰度,则更喜欢filter 然后map,或者当您过滤掉几乎所有内容但仍想保持功能正常时,更喜欢它以提高速度(所以不要'不想使用构建器);否则使用collect

【讨论】:

  • “过滤无”是指“过滤出无”,而不是“过滤器捕获/保留无”。第一个代码示例认为“阅读 [filter and map] 更清晰”。很高兴看到来自 100K 代表以北的如此详细的答案。
  • 看看在filter 之前添加.view 会如何改变事情会很有趣。
【解决方案2】:

我想这是相当基于意见的,但给出以下定义:

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

scala> def f(x: Int) = 2*x
f: (x: Int)Int

scala> def p(x: Int) = x%2 == 0
p: (x: Int)Boolean

你觉得这两个中哪一个更好读:

l.filter(p).map(f)

l.collect{ case i if p(i) => f(i) }

(请注意,我必须修正上面的语法,因为您需要括号和 case 来添加 if 条件)。

我个人觉得filter+map 更易于阅读和理解。这完全取决于您使用的确切语法,但考虑到pf,您在使用filtermap 时不必编写匿名函数,而在使用collect 时确实需要它们。

您也可以使用flatMap:

l.flatMap(i => if(p(i)) Some(f(i)) else None)

这可能是 3 种解决方案中最有效的,但我发现它不如 mapfilter 好。

总的来说,很难说哪个会更快,因为这取决于很多优化最终由scalac 执行,然后是 JVM。所有 3 个都应该非常接近,并且绝对不是决定使用哪一个的因素。

【讨论】:

  • 那么为什么这个论点不正确:“收集”就像一个单一的循环,“过滤器+地图”就像两个后果循环。因此(效率方面)人们应该更喜欢“收集”。我有什么遗漏吗。 (好在你提到了第三种选择。)
  • 与 Rex "Mr Collections" Kerr 相比是不公平的,但没有人会期望 flatMap 更高效。我个人的烦恼是使用 ell 作为 var 名称,因为它看起来像一个。现在,这是基于意见的!
【解决方案3】:

filter/map 看起来更干净的一种情况是您想要展平过滤器的结果

def getList(x: Int) = {
  List.range(x, 0, -1)
}

val xs = List(1,2,3,4)

//Using filter and flatMap
xs.filter(_ % 2 == 0).flatMap(getList)

//Using collect and flatten
xs.collect{ case x if x % 2 == 0 => getList(x)}.flatten

【讨论】:

  • 有时候flatMap你不需要filterxs.flatMap(n=&gt; if (n%2&lt;1) getList(n) else List())
猜你喜欢
  • 2011-11-03
  • 1970-01-01
  • 2011-03-18
  • 2011-06-18
  • 2010-12-03
  • 2015-08-13
  • 1970-01-01
  • 1970-01-01
  • 2013-02-21
相关资源
最近更新 更多