繁体   English   中英

Scala聚合函数的示例

[英]Example of the Scala aggregate function

我一直在寻找,我无法在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吗? 我不清楚这些参数是做什么的。

让我们来看看一些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

必须先计算每个内括号,然后再进行外括号。

聚合函数不会这样做(除了它是一个非常通用的函数,它可以用来做到这一点)。 你想要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。 as非空序列as = Seq(a1, ... an)A类元素, f: (A,A) => A是将A类型A两个元素合二为一的方法。 我会把它记为二元运算符@a1 @ a2而不是f(a1, a2) as.reduceLeft(@)将计算(((a1 @ a2) @ a3)... @ an) reduceRight会将括号放在另一个方向, (a1 @ (a2 @... @ an)))) 如果@恰好是关联的,则不关心括号。 人们可以将它计算为(a1 @... @ ap) @ (ap+1 @...@an) (在2个大的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,!,+)。

Aggregate非常接近。 然而,可能有一种更有效的方式来实现b@a不是b+a! 例如,如果类型BList[A] ,而b @ a是a :: b,则a! 将是a::Nil ,而b1 + b2将是b2 ::: b1 a :: b比(a :: Nil)::: b更好。 为了从关联性中受益,但仍然使用@ ,首先要分割b + a1! + ... + an! b + a1! + ... + an! ,进入(b + a1! + ap!) + (ap+1! + ..+ an!) ,然后回到使用@ with (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 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 (应用于已经部分组合的结果,如(z + a1@...@ap) + (z + ap+1@...@an) )。

总而言之, as.aggregate(z)(seqop, combop)计算与as.foldLeft(z)( seqop)相同的东西,前提是

  • (B, combop, z)是一个幺半群
  • seqop(b,a) = combop(b, seqop(z,a))

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

最后,使用aggregate解决最初的问题留给读者练习。 提示:使用foldLeft实现,然后找到满足上述条件的zcombo

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

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

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

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

aggregate就像foldLeft但可以并行执行。

正如missingfactor所说aggregate(z)(seqop, combop)的线性版本相当于foldleft(z)(seqop) 然而,在并行的情况下,这是不切实际的,我们不仅需要将下一个元素与前一个结果组合在一起(如在正常折叠中),而且我们希望将iterable拆分为我们称之为聚合的子迭代,并且需要再次结合这些。 (从左到右的顺序但不是关联的,因为我们可能在迭代的第一部分之前组合了最后的部分。)这种重新组合通常是非平凡的,因此,需要一种方法(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 ,其中combop使用两个子结果。

以下是关于如何使用基准标记在多核处理器上实现聚合启用性能的博客。 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并行集合

亚历山大·普罗科佩克

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

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

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实现默认情况下不支持此功能,但您可以在自己的代码中执行此操作。

TraversableOnce源中aggregate的定义是:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM