Scala聚合函数的示例

时间:2011-08-03 14:47:30

标签: scala aggregate-functions

我一直在寻找,我无法在Scala中找到我能理解的aggregate函数的示例或讨论。它看起来非常强大。

是否可以使用此函数来减少元组的值以生成多图类型集合?例如:

val list = Seq(("one", "i"), ("two", "2"), ("two", "ii"), ("one", "1"), ("four", "iv"))

应用聚合后:

Seq(("one" -> Seq("i","1")), ("two" -> Seq("2", "ii")), ("four" -> Seq("iv"))

另外,您能举例说明参数zsegopcombop吗?我不清楚这些参数是做什么的。

7 个答案:

答案 0 :(得分:92)

让我们看看一些ascii艺术是否有帮助。考虑aggregate的类型签名:

def aggregate [B] (z: B)(seqop: (B, A) ⇒ B, combop: (B, B) ⇒ B): B

另请注意,A指的是集合的类型。所以,假设我们在这个集合中有4个元素,那么aggregate可能会像这样工作:

z   A   z   A   z   A   z   A
 \ /     \ /seqop\ /     \ /    
  B       B       B       B
    \   /  combop   \   /
      B _           _ B
         \ combop  /
              B

让我们看一个实际的例子。假设我有GenSeq("This", "is", "an", "example"),我想知道其中有多少个字符。我可以写下面的内容:

请注意在以下代码段中使用par。传递给聚合的第二个函数是在计算各个序列之后调用的函数。 Scala只能对可以并行化的集合执行此操作。

import scala.collection.GenSeq
val seq = GenSeq("This", "is", "an", "example")
val chars = seq.par.aggregate(0)(_ + _.length, _ + _)

所以,首先它会计算出来:

0 + "This".length     // 4
0 + "is".length       // 2
0 + "an".length       // 2
0 + "example".length  // 7

接下来做的事情是无法预测的(组合结果的方法不止一种),但它可能会这样做(就像上面的ascii艺术一样):

4 + 2 // 6
2 + 7 // 9

最后以

结束
6 + 9 // 15

给出了最终结果。现在,这在结构上与foldLeft有点类似,但它有一个额外的函数(B, B) => B,折叠没有。但是,此功能使其能够并行工作!

例如,考虑四个计算中的每个计算初始计算彼此独立,并且可以并行完成。接下来的两个(结果为6和9)可以在完成它们所依赖的计算后启动,但这两个也可以并行运行。

如上所述并行化的7次计算可能只需要同时进行3次串行计算。

实际上,如此小的集合,同步计算的成本将足以消除任何收益。此外,如果你折叠它,它只需要 4 计算。然而,一旦你的收藏品变大,你就会开始看到一些真正的收获。

另一方面,考虑foldLeft。因为它没有附加功能,所以它无法并行化任何计算:

(((0 + "This".length) + "is".length) + "an".length) + "example".length

每个内括号必须在外部括号之前计算。

答案 1 :(得分:60)

聚合函数不会这样做(除了它是一个非常通用的函数,它可以用来做)。你想要groupBy。至少接近。当您从Seq[(String, String)]开始,并通过获取元组中的第一项((String, String) => String)进行分组时,它将返回Map[String, Seq[(String, String)])。然后,您必须丢弃Seq [String,String]]值中的第一个参数。

所以

list.groupBy(_._1).mapValues(_.map(_._2))

你得到Map[String, Seq[(String, String)]。如果您想要Seq而不是Map,请在结果上调用toSeq。我不认为你对Seq产生的订单有保证,但是


聚合是一项更难的功能。

首先考虑reduceLeft和reduceRight。 让asas = Seq(a1, ... an)类型元素的非空序列Af: (A,A) => A可以将类型A的两个元素合并为一个。我会将其记录为二元运算符@a1 @ a2而不是f(a1, a2)as.reduceLeft(@)将计算(((a1 @ a2) @ a3)... @ an)reduceRight将以相反的方式放置括号(a1 @ (a2 @... @ an))))。如果@恰好是关联的,则不关心括号。人们可以将它计算为(a1 @... @ ap) @ (ap+1 @...@an)(在两个大的parantheses中也会有parantheses,但是我们不关心它)。然后可以并行执行这两个部分,而reduceLeft或reduceRight中的嵌套包围强制执行完全顺序计算。但是,只有当@已知为关联时才能进行并行计算,而reduceLeft方法无法知道。

仍然可以有方法reduce,其调用者将负责确保操作是关联的。然后reduce将根据需要对调用进行排序,可能并行执行。的确,有这样一种方法。

然而,各种减少方法存在限制。 Seq的元素只能组合成相同类型的结果:@必须是(A,A) => A。但人们可能会遇到将它们组合成B的更普遍的问题。一个以b类型的值B开头,并将其与序列的每个元素组合。运算符@(B,A) => B,其中一个计算(((b @ a1) @ a2) ... @ an)foldLeft这样做。 foldRight做同样的事情,但从an开始。在那里,@操作没有机会成为关联的。当一个人写b @ a1 @ a2时,它必须表示(b @ a1) @ a2,因为(a1 @ a2)会输入错误。所以foldLeft和foldRight必须是顺序的。

但是,假设每个A可以转换为B,我们可以使用!来编写,a!的类型为B。另外假设有一个+操作(B,B) => B,而@实际上是b @ a b + a!。不是将元素与@组合,而是可以先使用!将所有元素转换为B,然后将它们与+组合。那将是as.map(!).reduceLeft(+)。如果+是关联的,那么可以使用reduce完成,而不是顺序:as.map(!)。reduce(+)。可能存在一种假设的方法as.associativeFold(b,!,+)。

聚合非常接近。但是,有一种更有效的方式来实现b@a而不是b+a!例如,如果类型BList[A],则b @ a是:: b,然后a!将为a::Nilb1 + b2将为b2 ::: b1。 a :: b比(a :: Nil)::: b更好。要从关联性中受益,但仍然使用@,首先将b + a1! + ... + an!拆分为(b + a1! + ap!) + (ap+1! + ..+ an!),然后使用@返回(b @ a1 @ an) + (ap+1! @ @ an)。一个还需要!在ap + 1上,因为必须以某些b开头。 +仍然是必要的,出现在parantheses之间。为此,as.associativeFold(!, +)可以更改为as.optimizedAssociativeFold(b, !, @, +)

返回++是关联的,或等效地,(B, +)是半群。在实践中,编程中使用的大多数半群也恰好是幺半群,即它们在B中包含中性元素z(对于),因此对于每个bz + b = b + z = b。在这种情况下,有意义的!操作可能是a! = z @ a。此外,因为z是b @ a1 ..@ an = (b + z) @ a1 @ an的中性元素b + (z + a1 @ an)。所以总是可以用z开始聚合。如果需要b,请在最后执行b + result。有了所有这些假设,我们可以做s.aggregate(z, @, +)。这就是aggregate的作用。 @seqop参数(应用于序列 z @ a1 @ a2 @ ap),+combop(已应用于已部分< em>组合结果,如(z + a1@...@ap) + (z + ap+1@...@an))。

总结一下,as.aggregate(z)(seqop, combop)计算的内容与as.foldLeft(z)( seqop)相同,只要

  • (B, combop, z)是monoid
  • seqop(b,a) = combop(b, seqop(z,a))

聚合实现可以使用combop的关联性来按照自己喜欢的方式对计算进行分组(但不是交换元素,+不能交换,:::不是)。它可以并行运行它们。

最后,使用aggregate解决初始问题仍然是读者的练习。提示:使用foldLeft实施,然后找到符合上述条件的zcombo

答案 2 :(得分:10)

具有A类元素的集合的签名是:

def aggregate [B] (z: B)(seqop: (B, A) ⇒ B, combop: (B, B) ⇒ B): B 
  • z是B类作为中性元素的对象。如果你想计算一些东西,你可以使用0,如果你想建立一个列表,从一个空列表开始,等等。
  • segop与您传递给fold方法的函数类似。它需要两个参数,第一个与您传递的中性元素的类型相同,表示在前一次迭代中已经聚合的东西,第二个是集合的下一个元素。结果还必须是B类型。
  • combop:是将两个结果合二为一的函数。

在大多数馆藏中,聚合在TraversableOnce中实现为:

  def aggregate[B](z: B)(seqop: (B, A) => B, combop: (B, B) => B): B 
    = foldLeft(z)(seqop)

因此combop被忽略。但是,并行集合是有意义的,因为seqop将首先在本地并行应用,然后调用combop来完成聚合。

因此,对于您的示例,您可以先尝试折叠:

val seqOp = 
  (map:Map[String,Set[String]],tuple: (String,String)) => 
    map + ( tuple._1 -> ( map.getOrElse( tuple._1, Set[String]() ) + tuple._2 ) )


list.foldLeft( Map[String,Set[String]]() )( seqOp )
// returns: Map(one -> Set(i, 1), two -> Set(2, ii), four -> Set(iv))

然后你必须找到一种折叠两个多图的方法:

val combOp = (map1: Map[String,Set[String]], map2: Map[String,Set[String]]) =>
       (map1.keySet ++ map2.keySet).foldLeft( Map[String,Set[String]]() ) { 
         (result,k) => 
           result + ( k -> ( map1.getOrElse(k,Set[String]() ) ++ map2.getOrElse(k,Set[String]() ) ) ) 
       } 

现在,您可以并行使用聚合:

list.par.aggregate( Map[String,Set[String]]() )( seqOp, combOp )
//Returns: Map(one -> Set(i, 1), two -> Set(2, ii), four -> Set(iv))

将方法“par”应用于list,从而使用列表的并行集合(scala.collection.parallel.immutable.ParSeq)来真正利用多核处理器。如果没有“par”,则不会有任何性能提升,因为并行集合上没有聚合。

答案 3 :(得分:9)

aggregatefoldLeft类似,但可以并行执行。

作为missingfactor saysaggregate(z)(seqop, combop)的线性版本等同于foldleft(z)(seqop)。然而,在并行的情况下,这是不切实际的,我们不仅需要将下一个元素与前一个结果组合在一起(如在正常折叠中),而且我们希望将iterable拆分为我们称之为聚合的子迭代,并且需要再次结合这些。 (从左到右的顺序但不是关联的,因为我们可能在迭代的第一部分之前组合了最后的部分。)这种重新组合通常是非平凡的,因此,需要一个方法{{1}实现这一点。

(S, S) => S中的定义是:

ParIterableLike

确实使用def aggregate[S](z: S)(seqop: (S, T) => S, combop: (S, S) => S): S = { executeAndWaitResult(new Aggregate(z, seqop, combop, splitter)) }

作为参考,combop定义为:

Aggregate

重要的部分是protected[this] class Aggregate[S](z: S, seqop: (S, T) => S, combop: (S, S) => S, protected[this] val pit: IterableSplitter[T]) extends Accessor[S, Aggregate[S]] { @volatile var result: S = null.asInstanceOf[S] def leaf(prevr: Option[S]) = result = pit.foldLeft(z)(seqop) protected[this] def newSubtask(p: IterableSplitter[T]) = new Aggregate(z, seqop, combop, p) override def merge(that: Aggregate[S]) = result = combop(result, that.result) } ,其中merge应用了两个子结果。

答案 4 :(得分:3)

以下是关于如何使用基准测试在多核处理器上实现聚合启用性能的博客。 http://markusjais.com/scalas-parallel-collections-and-the-aggregate-method/

以下是“Scala Days 2011”中关于“Scala parallel collections”的视频。 http://days2011.scala-lang.org/node/138/272

视频说明

Scala Parallel Collections

Aleksandar Prokopec

随着处理器内核数量的增长,并行编程抽象变得越来越重要。高级编程模型使程序员能够更多地关注程序,而不是关注同步和负载平衡等低级细节。 Scala并行集合扩展了Scala集合框架的编程模型,提供了对数据集的并行操作。 该演讲将描述并行收集框架的体系结构,解释它们的实现和设计决策。将描述诸如并行散列映射和并行散列尝试的具体集合实现。最后,将展示几个示例应用程序,在实践中演示编程模型。

答案 5 :(得分:1)

aggregate来源中TraversableOnce的定义是:

def aggregate[B](z: B)(seqop: (B, A) => B, combop: (B, B) => B): B = 
  foldLeft(z)(seqop)

与简单foldLeft没什么不同。似乎没有在任何地方使用combop。我自己很困惑这个方法的目的是什么。

答案 6 :(得分:1)

只是为了澄清对我之前的解释,理论上的想法就是这样 聚合应该像这样工作,(我已经更改了参数的名称,使它们更清晰):

Seq(1,2,3,4).aggragate(0)(
     addToPrev = (prev,curr) => prev + curr, 
     combineSums = (sumA,sumB) => sumA + sumB)

应逻辑转换为

Seq(1,2,3,4)
    .grouped(2) // split into groups of 2 members each
    .map(prevAndCurrList => prevAndCurrList(0) + prevAndCurrList(1))
    .foldLeft(0)(sumA,sumB => sumA + sumB)

由于聚合和映射是分开的,原始列表理论上可以分成不同大小的不同组,并行运行或甚至在不同的机器上运行。 实际上,scala current实现默认情况下不支持此功能,但您可以在自己的代码中执行此操作。