web-dev-qa-db-ja.com

Scala集約関数の例

私は探していましたが、aggregate関数の例や説明をScalaで理解できます。かなり強力なようです。

この関数を使用して、タプルの値を減らしてマルチマップタイプのコレクションを作成できますか?例えば:

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"))

また、パラメータzsegop、およびcombopの例を挙げることができますか?これらのパラメーターが何をするのかはわかりません。

35
christangrant

集約関数はそれを行いません(非常に一般的な関数であり、それを行うために使用できることを除いて)。 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を検討してください。 asA型の要素の空でないシーケンスas = Seq(a1, ... an)とし、f: (A,A) => AAを1つに。 f(a1, a2)ではなく、バイナリ演算子_@_、_a1 @ a2_として注意します。 as.reduceLeft(@)は_(((a1 @ a2) @ a3)... @ an)_を計算します。 reduceRightは括弧を別の方法で配置します、_(a1 @ (a2 @... @ an))))_。 _@_が偶然結合する場合、括弧は気にしません。 _(a1 @... @ ap) @ (ap+1 @...@an)_として計算することができます(2つの大きなパラセシスの中にもパラセシスがありますが、それについては気にしません)。次に、reduceLeftまたはreduceRightのネストされたブラケットによって完全にシーケンシャルな計算を強制しながら、2つの部分を並行して実行できます。しかし、並列計算は_@_が結合的であることがわかっている場合にのみ可能であり、reduceLeftメソッドはそれを知ることができません。

それでも、メソッドreduceが存在する可能性があります。このメソッドの呼び出し元は、操作が連想されるようにする責任があります。次に、reduceは適切と思われる呼び出しを並べ、おそらく並行して実行します。確かに、そのような方法があります。

ただし、さまざまなreduceメソッドには制限があります。 Seqの要素は、同じタイプの結果にのみ結合できます。_@_は_(A,A) => A_でなければなりません。しかし、それらをBに結合するというより一般的な問題が発生する可能性があります。タイプbの値Bで始まり、それをシーケンスのすべての要素と組み合わせます。演算子_@_は_(B,A) => B_であり、1つは_(((b @ a1) @ a2) ... @ an)_を計算します。 foldLeftはそれを行います。 foldRightは同じことをしますが、anで始まります。そこでは、_@_操作は結合する機会がありません。 _b @ a1 @ a2_を記述するときは、_(b @ a1) @ a2_の型が間違っているため、_(a1 @ a2)_を意味する必要があります。したがって、foldLeftとfoldRightは連続している必要があります。

ただし、各ABに変換できると仮定して、_!_、_a!_はB型です。さらに、_+_操作_(B,B) => B_があり、_@_が_b @ a_が実際に_b + a!_であると仮定します。要素を@と結合するのではなく、最初に_!_を使用してすべての要素をBに変換し、次に_+_を使用してそれらを結合できます。それはas.map(!).reduceLeft(+)です。そして、_+_が結合的である場合、それは、reduceで行うことができ、順次ではありません:as.map(!)。reduce(+)。架空のメソッドas.associativeFold(b、!、+)があります。

集計はそれに非常に近いです。ただし、_b@a_よりも_b+a!_を実装する方が効率的な方法がある場合があります。たとえば、タイプBが_List[A]_であり、b @ aがaである場合: :b、その後_a!_は_a::Nil_になり、_b1 + 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から始める必要があるためです。また、+記号も必要です。これは、括弧の間に表示されます。これを行うには、as.associativeFold(!, +)as.optimizedAssociativeFold(b, !, @, +)に変更できます。

_+_に戻ります。 _+_は結合的、または同等に、_(B, +)_はセミグループです。実際には、プログラミングで使用されるセミグループのほとんどはモノイドでもあります。つまり、Bの中立要素zzero)を含むため、各b、_z + b_ = _b + z_ = b。その場合、意味のある_!_操作は、おそらく_a! = z @ a_になります。さらに、zはb @ a1 ..@ an = (b + z) @ a1 @ anである中立要素b + (z + a1 @ an)であるためです。したがって、zを使用して集計を開始することは常に可能です。代わりにbが必要な場合は、最後に_b + result_を実行します。これらすべての仮説で、as.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))

集約実装は、コンボの結合性を使用して、計算を好きなようにグループ化できます(ただし、要素を交換しないでください、+は可換である必要はありません、::はそうではありません)。それらを並行して実行できます。

最後に、aggregateを使用して初期問題を解決することは、読者への課題として残されています。ヒント:foldLeftを使用して実装し、上記の条件を満たすzおよびcomboを見つけます。

60
Didier Dupont

いくつかのアスキーアートが役に立たないか見てみましょう。 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を使用していることに注意してください。集計に渡される2番目の関数は、個々のシーケンスが計算された後に呼び出されます。 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_があり、foldにはありません。ただし、この関数を使用すると、並行して動作します!

たとえば、4つの計算の初期計算はそれぞれ独立しており、並行して実行できることを考慮してください。次の2つ(結果は6と9)は、依存する計算が終了すると開始できますが、これら2つは()並行して実行できます。

上記のように並列化された7つの計算は、3つのシリアル計算と同じ時間で済みます。

実際、このような小さなコレクションでは、計算の同期にかかるコストは、利益を一掃するのに十分な大きさになります。さらに、これを折り畳んだ場合、4計算の合計のみがかかります。ただし、コレクションが大きくなると、実際の利益が見られるようになります。

一方、foldLeftを検討してください。追加の機能がないため、計算を並列化できません。

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

外側の括弧を進める前に、内側の括弧をそれぞれ計算する必要があります。

93

タイプAの要素を持つコレクションのシグネチャは次のとおりです。

def aggregate [B] (z: B)(seqop: (B, A) ⇒ B, combop: (B, B) ⇒ B): B 
  • zは、中立要素として機能するタイプBのオブジェクトです。何かを数えたい場合は、0を使用できます。リストを作成したい場合、空のリストから始めたい場合などです。
  • segopは、foldメソッドに渡す関数に類似しています。 2つの引数を取ります。最初の引数は、渡したニュートラル要素と同じ型で、前の反復で既に集約されたものを表します。2番目の引数は、コレクションの次の要素です。結果もB型でなければなりません。
  • combop:は、2つの結果を1つに結合する関数です。

ほとんどのコレクションでは、集約は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))

次に、2つのマルチマップを折りたたむ方法を見つける必要があります。

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」がなければ、集約は並列コレクションで行われないため、パフォーマンスは向上しません。

10
paradigmatic

aggregatefoldLeftに似ていますが、並行して実行できます。

missingfactor says のように、aggregate(z)(seqop, combop)の線形バージョンはfoldleft(z)(seqop)と同等です。しかし、これは、次の要素を前の結果と結合する必要がある並列ケースでは非現実的です(通常の折り畳みのように)が、iterableをsubiterablesに分割します。それらを再び組み合わせます。 (左から右の順序ですが、反復可能ファイルの最初の部分の前に最後の部分を結合した可能性があるため、結合ではありません。)この再結合は一般的に自明ではないため、メソッド(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は2つのサブ結果で適用されます。

9
Debilski

以下に、ベンチマークを使用して、マルチコアプロセッサで集計がパフォーマンスをどのように有効にするかに関するブログを示します。 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コレクションフレームワークのプログラミングモデルを拡張し、データセットの並列操作を提供します。講演では、並列コレクションフレームワークのアーキテクチャについて説明し、並列ハッシュマップや並列ハッシュ試行などの具体的なコレクションの実装について説明し、最後に、実際のプログラミングモデルを示すいくつかのアプリケーション例を示します。

3
Win Myo Htet

aggregateソースのTraversableOnceの定義は次のとおりです。

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

単純なfoldLeftと違いはありません。 combopはどこでも使用されていないようです。この方法の目的が何なのか、私自身は混乱しています。

1
missingfaktor

私の前にある人の説明を明確にするために、理論的には、集約は次のように機能するはずだという考え方です(より明確にするためにパラメータの名前を変更しました)。

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現在の実装はデフォルトでこの機能をサポートしていませんが、独自のコードでこれを行うことができます。

1
Micheal Kris