함수형 프로그래밍 (특히 Scala 및 Scala API)에서 reduce와 foldLeft / fold의 차이점은 무엇입니까?


답변:


260

축소 대 foldLeft

이 주제와 관련된 다른 stackoverflow 답변에서 명확하게 언급되지 않은 큰 차이점 reducecommutative monoid , 즉 commutative 및 associative 연산을 제공해야한다는 것 입니다. 이는 작업을 병렬화 할 수 있음을 의미합니다.

이러한 구분은 빅 데이터 / MPP / 분산 컴퓨팅에있어 매우 중요하며, 그 이유 reduce가 모두 존재합니다. 컬렉션은 잘게 쪼개 질 수 있고 reduce각 청크에서 reduce작동 할 수 있습니다. 그런 다음 각 청크 의 결과에 대해 작동 할 수 있습니다. 사실 청킹 수준은 한 수준 깊이에서 멈출 필요가 없습니다. 우리는 각 덩어리도자를 수 있습니다. 이것이 무한한 수의 CPU가 주어지면 목록의 정수 합계가 O (log N) 인 이유입니다.

방금 서명을 보면 대한 이유가 없다 reduce당신과 당신이 할 수있는 모든 것을 달성 할 수 있기 때문에 존재하는 reduce와 함께 foldLeft. 의 기능이의 기능 foldLeft보다 큽니다 reduce.

그러나 를 병렬화 할 수 없으므로 foldLeft런타임은 항상 O (N)입니다 (교류 형 모노 이드를 공급하더라도). 이는 연산이 교환 적 모노 이드 가 아니라고 가정하고 누적 된 값이 일련의 순차적 집계에 의해 계산되기 때문입니다.

foldLeftcommutativity 또는 associativity를 가정하지 않습니다. 컬렉션을 다듬을 수있는 능력을 부여하는 것은 연관성이고 순서가 중요하지 않기 때문에 누적을 쉽게 만드는 것은 교환 성입니다 (따라서 각 청크에서 각 결과를 집계 할 순서는 중요하지 않습니다). 엄밀히 말하면, 병렬화에는 commutativity가 필요하지 않습니다 (예 : 분산 정렬 알고리즘). 청크에 순서를 지정할 필요가 없기 때문에 논리를 더 쉽게 만듭니다.

Spark 문서를 reduce보면 "... 교환 및 연관 이항 연산자"라고 명시되어 있습니다.

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

다음은 reduce특별한 경우가 아니라는 증거 입니다.foldLeft

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

축소 대 접기

이제 이것은 FP / 수학적 뿌리에 조금 더 가까워지고 설명하기가 조금 더 까다로워지는 곳입니다. Reduce는 순서없는 컬렉션 (다중 집합)을 다루는 MapReduce 패러다임의 일부로 공식적으로 정의되고, Fold는 재귀 측면에서 공식적으로 정의되며 (카타 모피 즘 참조) 따라서 컬렉션에 대한 구조 / 시퀀스를 가정합니다.

더 없습니다 fold때문에 우리가 정의 할 수 없습니다 프로그래밍 모델을 줄이려면 (엄격한)지도 아래 끓는에서 방법 fold덩어리가 순서를 가지고 있지 않기 때문에 fold유일한 연관성이 아니라 교환 법칙이 필요가.

간단히 말해서, reduce누적 순서없이 작동하고, 누적 순서가 fold필요하며,이를 구별하는 0 값의 존재가 아니라 0 값을 필요로하는 것은 누적 순서입니다. 엄밀히 말하면 빈 컬렉션에서 작동 reduce 해야 합니다. 왜냐하면 0 값은 임의의 값을 취한 다음를 x풀어서 추론 할 수 있기 때문 x op y = x입니다. (예 x op y != y op x). 물론 스칼라는 (아마도 계산할 수없는) 수학을해야하기 때문에이 제로 값이 무엇인지 알아 내려고하지 않습니다. 그래서 그냥 예외를 던집니다.

프로그래밍의 유일한 명백한 차이점은 서명이기 때문에이 원래의 수학적 의미가 상실된 것 같습니다 (어원학의 경우가 많습니다). 그 결과 MapReduce의 원래 의미를 보존하기보다 reduce의 동의어가되었습니다 fold. 이제 이러한 용어는 종종 같은 의미로 사용되며 대부분의 구현에서 동일하게 작동합니다 (빈 컬렉션 무시). 이상 함은 Spark와 같은 특이성에 의해 악화됩니다.

따라서 Spark 에는fold있지만 하위 결과 (각 파티션에 대해 하나씩)가 결합되는 순서 (작성 당시)는 작업이 완료되는 순서와 동일하므로 비 결정적입니다. 지적에 대한 @CafeFeed 덕분에 fold사용 runJob후 코드를 읽고, 나는 그것이 비 결정적이다 것을 깨달았다. 또한 혼란은 스파크가없는 의해 생성 treeReduce되지만를 treeFold.

결론

사이에 차이가 reducefold도 비어 있지 않은 시퀀스에 적용이. 전자는 임의의 순서 ( http://theory.stanford.edu/~sergei/papers/soda10-mrc.pdf )를 사용하여 컬렉션에 대한 MapReduce 프로그래밍 패러다임의 일부로 정의되며 연산자는 결정 론적 결과를 제공하기 위해 연관됩니다. 후자는 catomorphisms 관점에서 정의되며 컬렉션이 시퀀스 개념을 가져야하며 (또는 연결된 목록처럼 재귀 적으로 정의 됨) 교환 연산자가 필요하지 않습니다.

실제로 때문에 프로그래밍의 unmathematical 특성, reducefold하나 제대로 (스칼라처럼) 또는 잘못 (불꽃처럼), 같은 방식으로 행동하는 경향이있다.

추가 : Spark API에 대한 나의 의견

내 의견은 foldSpark에서 용어 사용 이 완전히 삭제되면 혼란을 피할 수 있다는 것 입니다. 적어도 spark는 문서에 메모가 있습니다.

이것은 Scala와 같은 기능적 언어에서 분산되지 않은 컬렉션에 대해 구현 된 접기 작업과는 다소 다르게 작동합니다.


2
그것이 그 이름에를 foldLeft포함하는 이유 Left와라는 메서드도있는 이유 fold입니다.
kiritsuku

1
@Cloudtech 이것은 사양이 아닌 단일 스레드 구현의 우연입니다. 제 4 코어 시스템에서, 내가 추가하려고하면 .par, 그래서 (List(1000000.0) ::: List.tabulate(100)(_ + 0.001)).par.reduce(_ / _)내가 매번 다른 결과를 얻을 수 있습니다.
samthebest

2
컴퓨터 과학의 맥락에서 @AlexDean은 빈 컬렉션이 예외를 던지는 경향이 있기 때문에 실제로 ID가 필요하지 않습니다. 그러나 컬렉션이 비어있을 때 identity 요소가 반환되면 수학적으로 더 우아합니다 (컬렉션이 이렇게하면 더 우아합니다). 수학에서 "예외를 던져라"는 존재하지 않습니다.
samthebest 2014

3
@samthebest : 정류성에 대해 확신하십니까? github.com/apache/spark/blob/… 은 " 교류 형 이 아닌 함수의 경우 결과가 분산되지 않은 컬렉션에 적용된 폴드의 결과와 다를 수 있습니다."라고 말합니다.
Make42

1
@ Make42 맞습니다. reallyFold하지만 다음과 같이 자신의 포주를 작성할 수 있습니다 rdd.mapPartitions(it => Iterator(it.fold(zero)(f)))).collect().fold(zero)(f). 출퇴근하는 데 f가 필요하지 않습니다.
samthebest

10

내가 착각하지 않았다면 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


몇 번을 앞뒤로 살펴보면 귀하가 옳다고 믿습니다. 결합 순서는 선착순입니다. sc.makeRDD(0 to 9, 2).mapPartitions(it => { java.lang.Thread.sleep(new java.util.Random().nextInt(1000)); it } ).map(_.toString).fold("")(_ + _)2 개 이상의 코어로 여러 번 실행 하면 무작위 (파티션 단위) 순서가 생성되는 것을 볼 수있을 것입니다. 그에 따라 내 답변을 업데이트했습니다.
samthebest

3

foldApache Spark에서는 fold분산되지 않은 컬렉션 과 동일 하지 않습니다. 실제로 결정 론적 결과를 생성 하려면 교환 함수필요 합니다.

이것은 Scala와 같은 기능적 언어에서 분산되지 않은 컬렉션에 대해 구현 된 접기 작업과는 다소 다르게 작동합니다. 이 접기 작업은 파티션에 개별적으로 적용한 다음 정의 된 순서대로 각 요소에 순차적으로 접기를 적용하는 대신 결과를 최종 결과로 접을 수 있습니다. 교환되지 않는 함수의 경우 결과가 분산되지 않은 컬렉션에 적용된 접기의 결과와 다를 수 있습니다.

표시되었습니다 에 의해 미사 엘 로젠탈 에 의해 제안 Make42자신의 의견 .

관찰 된 동작은 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)

설명 :

foldRDD의 구조

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
}

RDD의 구조reduce 와 동일합니다 .

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하고 foldLeftTraversableOnce.

결론 : foldon RDD는 청크의 순서에 의존 할 수 없으며 commutativity 및 associativity가 필요합니다 .


어원이 혼란스럽고 프로그래밍 문헌에는 공식적인 정의가 부족하다는 것을 인정해야합니다. 나는 foldon RDDs가 실제로 reduce. 나는 그들의 파티오가하는 일이 무엇이든 자신감을 가지고 있다면 우리가 정말로 commutativity 가 필요하다는 것에 동의하지 않지만 , 그것은 질서를 보존하는 것입니다.
samthebest

정의되지 않은 접기 순서는 분할과 관련이 없습니다. runJob 구현의 직접적인 결과입니다.

아! 죄송합니다. 요점이 무엇인지 알아낼 수 없었지만 runJob코드 를 읽어 보니 실제로 파티션 순서가 아니라 작업이 완료 될 때 결합이 수행된다는 것을 알 수 있습니다. 모든 것을 제자리에 놓는 것은이 핵심 세부 사항입니다. 나는 내 대답을 다시 편집 하여 지적한 실수를 수정했습니다. 우리가 지금 합의 했으니 현상금을 제거 할 수 있습니까?
samthebest

편집하거나 제거 할 수 없습니다. 이러한 옵션이 없습니다. 내가 수여 할 수는 있지만 관심만으로도 꽤 많은 점수를받는 것 같아요, 제가 틀렸나 요? 내가 보상을 받기를 원한다고 확인하면 다음 24 시간 내에 처리합니다. 수정 해주셔서 감사하고 방법에 대해 미안하지만 모든 경고를 무시한 것처럼 보였고 큰 일이며 답변이 곳곳에 인용되었습니다.

1
처음으로 우려 사항을 명확히 밝힌 이후 @Mishael Rosenthal에게 수여하는 것은 어떻습니까? 나는 포인트에 관심이 없으며 SEO 및 조직에 SO를 사용하는 것을 좋아합니다.
samthebest

2

Scalding의 또 다른 차이점은 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에서 작업을 monoid로 정의하는 것이 좋습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.