web-dev-qa-db-ja.com

関数型プログラミングにおけるreduceとfoldLeft / foldの違い(特にScalaとScala API)?

なぜScalaと、SparkとScaldingのようなフレームワークにはreducefoldLeftの両方があるのでしょうか?では、reduceおよびfold

92
samthebest

reduce vs foldLeft

このトピックに関連する他のスタックオーバーフローの答えで明確に言及されていない大きな大きな違いは、reduce可換モノイド、つまり可換および結合の両方の操作を与える必要があることですこれは、操作を並列化できることを意味します。

この区別は、ビッグデータ/ MPP /分散コンピューティング、およびreduceが存在する理由全体にとって非常に重要です。コレクションを切り刻み、reduceを各チャンクで操作し、reduceで各チャンクの結果を操作できます。実際、チャンキングのレベルは1レベル深くする必要はありません。各チャンクも切り刻むことができます。これが、無限数のCPUが与えられた場合、リスト内の整数の合計がO(log N)である理由です。

署名だけを見ると、reduceが存在する理由はありません。なぜなら、reduceを使用してfoldLeftでできることはすべて達成できるからです。 foldLeftの機能は、reduceの機能よりも優れています。

ただし、foldLeftを並列化することはできません。そのため、ランタイムは常にO(N)です(可換モノイドを入力した場合でも)。これは、操作がnot可換モノイドであると想定されているため、累積値は一連の連続的な集計によって計算されるためです。

foldLeftは、可換性も結合性も想定していません。コレクションを切り刻む機能を提供するのは結合性であり、順序は重要ではないため、累積は簡単になります(したがって、各チャンクからの各結果を集約する順序は関係ありません)。厳密に言えば、分散ソートアルゴリズムなどの並列化には、厳密に言えば可換性は必要ありません。チャンクに順序を付ける必要がないため、ロジックが簡単になります。

reduceのSparkドキュメントを見ると、具体的には「...可換および連想バイナリ演算子」と書かれています。

http://spark.Apache.org/docs/1.0.0/api/scala/index.html#org.Apache.spark.rdd.RDD

reducefoldLeftの単なる特殊なケースではないことの証明です。

scala> val intParList: ParSeq[Int] = (1 to 100000).map(_ => scala.util.Random.nextInt()).par

scala> timeMany(1000, intParList.reduce(_ + _))
Took 462.395867 milli seconds

scala> timeMany(1000, intParList.foldLeft(0)(_ + _))
Took 2589.363031 milli seconds

リデュースvsフォールド

これは、FP /数学的なルートに少し近づき、説明するのが少し難しい場所です。 Reduceは、順序のないコレクション(マルチセット)を扱うMapReduceパラダイムの一部として正式に定義されています。

Scaldingにはfoldメソッドがありません。(厳密な)Map Reduceプログラミングモデルでは、foldを定義できないためです。チャンクには順序がなく、foldには可換性ではなく結合性のみが必要です。

簡単に言うと、reduceは累積の順序なしで機能します。foldは累積の順序を必要とし、ゼロの値を必要とするのは累積の順序です。厳密に言えばreduceshouldは空のコレクションで動作します。これは、任意の値xを取得してからx op y = xを解くことでそのゼロ値を推測できるためです別個の左右のゼロ値が存在する可能性があるため、非可換演算を使用します(つまり、x op y != y op x)。もちろん、Scalaは、このゼロ値が何であるかを計算することに煩わされないため、数学(おそらく計算できない)を行う必要があるため、例外をスローするだけです。

プログラミングの唯一の明白な違いは署名であるため、この元の数学的意味は失われているようです(語源の場合によくあることです)。その結果、reduceはMapReduceの本来の意味を保持するのではなく、foldの同義語になりました。現在、これらの用語は互換的に使用されることが多く、ほとんどの実装で同じように動作します(空のコレクションを無視します)。奇妙さは、Sparkのような特異性によって悪化します。

Sparkdoesにはfoldがありますが、サブ結果(各パーティションに1つ)が結合される順序(執筆時点)は、同じ順序ですどのタスクが完了するか-したがって、非決定的です。 foldrunJobを使用していることを指摘してくれた@CafeFeedに感謝します。これは、コードを読んだ後、非決定的であることに気付きました。 SparkがtreeReduceを持ち、treeFoldを持たないことにより、さらに混乱が生じます。

結論

空でないシーケンスに適用される場合でも、reducefoldには違いがあります。前者は、任意の順序( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf )のコレクションに関するMapReduceプログラミングパラダイムの一部として定義されており、演算子は、決定論的な結果を得るために結合的であることに加えて、可換的です。後者はカトモルフィズムの観点から定義されており、コレクションにシーケンスの概念がある(またはリンクリストのように再帰的に定義される)必要があるため、可換演算子は必要ありません。

実際には、プログラミングの非数学的な性質のため、reducefoldは、正しく(Scalaのように)または間違って(Sparkのように)同じように動作する傾向があります。

追加:Spark APIに関する私の意見

私の意見では、用語foldの使用がSparkで完全に削除された場合、混乱は回避されると考えられます。少なくともsparkのドキュメントにはメモがあります。

これは、Scalaのような関数型言語の非分散コレクションに実装されたフォールド操作とは多少異なる動作をします。

249
samthebest

間違っていなければ、Spark APIはそれを必要としませんが、foldはfが可換であることも必要とします。パーティションが集約される順序は保証されないためです。次のコードの例では、最初の印刷のみがソートされます。

import org.Apache.spark.{SparkConf, SparkContext}

object FoldExample extends App{

  val conf = new SparkConf()
    .setMaster("local[*]")
    .setAppName("Simple Application")
  implicit val sc = new SparkContext(conf)

  val range = ('a' to 'z').map(_.toString)
  val rdd = sc.parallelize(range)

  println(range.reduce(_ + _))
  println(rdd.reduce(_ + _))
  println(rdd.fold("")(_ + _))
}  

プリントアウト:

abcdefghijklmnopqrstuvwxyz

abcghituvjklmwxyzqrsdefnop

defghinopjklmqrstuvabcwxyz

10

Scaldingのもう1つの違いは、Hadoopでのコンバイナーの使用です。

あなたの操作がreduceで可換モノイドであり、すべてのデータをリデューサーにシャッフル/ソートする代わりにマップ側にも適用されると想像してください。 foldLeftの場合、これは当てはまりません。

pipe.groupBy('product) {
   _.reduce('price -> 'total){ (sum: Double, price: Double) => sum + price }
   // reduce is .mapReduceMap in disguise
}

pipe.groupBy('product) {
   _.foldLeft('price -> 'total)(0.0){ (sum: Double, price: Double) => sum + price }
}

Scaldingでは、操作をモノイドとして定義することを常にお勧めします。

2
morazow

Apacheのfold Sparkは、非分散コレクションのfoldと同じではありません。実際には 交換関数が必要です を生成します決定的な結果:

これは、Scalaのような関数型言語の非分散コレクションに実装されたフォールド操作とは多少異なる動作をします。この折り畳み操作はパーティションに個別に適用でき、定義された順序で各要素に連続して折り畳みを適用するのではなく、それらの結果を最終結果に折り畳むことができます。可換でない関数の場合、結果は、非分散コレクションに適用されるフォールドの結果と異なる場合があります。

これは 表示されています によって Mishael Rosenthal によって提案され、 Make42 in his comment で示されています。

推奨されています 実際にHashPartitionerがシャッフルせず、parallelizeを使用しない場合、観察された動作はHashPartitionerに関連します。

import org.Apache.spark.sql.SparkSession

/* Note: standalone (non-local) mode */
val master = "spark://...:7077"  

val spark = SparkSession.builder.master(master).getOrCreate()

/* Note: deterministic order */
val rdd = sc.parallelize(Seq("a", "b", "c", "d"), 4).sortBy(identity[String])
require(rdd.collect.sliding(2).forall { case Array(x, y) => x < y })

/* Note: all posible permutations */
require(Seq.fill(1000)(rdd.fold("")(_ + _)).toSet.size == 24)

説明:

foldの構造 RDDの場合

def fold(zeroValue: T)(op: (T, T) => T): T = withScope {
  var jobResult: T
  val cleanOp: (T, T) => T
  val foldPartition = Iterator[T] => T
  val mergeResult: (Int, T) => Unit
  sc.runJob(this, foldPartition, mergeResult)
  jobResult
}

同じ reduceの構造として RDDの場合:

def reduce(f: (T, T) => T): T = withScope {
  val cleanF: (T, T) => T
  val reducePartition: Iterator[T] => Option[T]
  var jobResult: Option[T]
  val mergeResult =  (Int, Option[T]) => Unit
  sc.runJob(this, reducePartition, mergeResult)
  jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
}

runJobはパーティションの順序を無視して実行されるため、可換関数が必要になります。

foldPartitionreducePartitionは、処理順序の点で同等であり、 reduceLeft および foldLeft on TraversableOnce

結論:RDDのfoldは、チャンクの順序とニーズの可換性と結合性に依存できません。

2
user6022341