본문 바로가기

Kotlin

[Kotlin] reduce()와 fold()

개요

Collection에서 제공하는 메소드에서 reduce()fold()의 차이를 알아봅니다.


내용

reduce()fold() 모두 accumulation(누산) 작업이 요구될 때 사용하는 Collection 메소드입니다.

두 메소드 모두 Collection의 요소들을 차례로 누적하여 하나의 값으로 만드는데 사용됩니다.

 

하지만 초기값 지정 여부에 따라 중요한 차이가 있습니다.

  • reduce() : 초기값 지정 불가 (첫 번째 요소를 초기값으로 사용)
  • fold() : 초기값 지정 가능 (명시적으로 지정된 초기값 사용)

이로 인해 반환되는 결과가 다릅니다.

 

1. 기본 동작

val numbers = listOf(1, 2, 3)

val sumByReduce = numbers.reduce { acc, num -> acc + num }
println("sum with reduce() : ${sumByReduce}") // sum with reduce() : 6

val sumByFold = numbers.reduce(10) { acc, num -> acc + num }
println("sum with fold() : ${sumByFold}") // sum with reduce() : 16

✅reduce()

Collection의 첫 번째 요소를 초기값으로 사용하며 이후의 요소들을 순차적으로 처리합니다.

 

fold()

명시적으로 초기값을 지정하며 이를 기준으로 연산을 시작합니다.

 

2. 빈 Collection에서 reduce()와 fold()

val numbers = emptyList<Int>()

val sumByFold = numbers.fold(10) { acc, num -> acc + num }
println("sum with fold() : ${sumByFold}")

val sumByReduce = numbers.reduce { acc, num -> acc + num }
println("sum with reduce() : ${sumByReduce}")

// result
folded: 10

Empty collection can't be reduced.
java.lang.UnsupportedOperationException: Empty collection can't be reduced.
	at kr.leocat.test.FoldTest.test(FoldTest.kt:35)
  ...

fold()

초기값만 반환하면 되기 때문에 안전하게 결과를 반환합니다.

 

reduce()

빈 Collection에서 초기값을 지정할 수 없으므로 예외가 발생합니다.

 

즉, 빈 Collection을 처리해야 하는 경우 fold()를 사용하는 것이 안전합니다.

 

3. 첫 번째 요소의 차이

reduce()fold()가 첫 번째 요소로 사용하는 데이터도 서로 차이가 있습니다.

메소드 내부에서 변수 선언을 (acc, num)으로 했다면

  • reduce()는 Collection의 첫 번째를 acc로, 두 번째 요소를 num으로 사용합니다.
  • fold()는 초기값을 acc로, Collection의 첫 번째 요소를 num으로 사용합니다.

따라서 다음과 같은 코드는 결과가 다릅니다.

 

아래의 코드는 리스트에 있는 데이터에 각각 2를 곱하고 모두 더하는 게 목표입니다.

이때 reduce()는 11을 반환하고 fold()는 12를 반환합니다.

val numbers = listOf(1, 2, 3)

val sumByReduce = numbers.reduce { acc, num -> acc + num * 2 }
println("sum with reduce() : ${sumByReduce}") // sum with reduce() : 11

val sumByFold = numbers.fold(0) { acc, num -> acc + num * 2 }
println("sum with fold() : ${sumByFold}") // sum with fold() : 12

reduce() 연산 과정

  1. acc = 1, num = 21 + (2 * 2) = 5
  2. acc = 5, num = 35 + (3 * 2) = 11

fold() 연산 과정

  1. acc = 0, num = 10 + (1 * 2) = 2
  2. acc = 2, num = 22 + (2 * 2) = 6
  3. acc = 6, num = 36 + (3 * 2) = 12

초기값의 존재 여부가 연산 순서와 결과에 영향을 미칩니다.

 

4. Java의 Stream API와의 차이

List<Integer> numbers = ImmutableList.of(1,2,3);

Optional<Integer> sum = numbers.stream()
			.reduce((total, num) -> total + num); // Integer::sum
System.out.println("reduced: " + sum.get());
Integer sumFromTen = numbers.stream()
			.reduce(10, (total, num) -> total + num);
System.out.println("folded: " + sumFromTen);

Kotlin에서는 reduce()fold()를 명확하게 분리했지만,

Java의 Stream API에서는 위와 같이 reduce()의 오버로딩을 통해 초기값을 지정할 수 있습니다.

 

Java에서는 초기값이 없는 reduce()를 사용하면 Optional<Integer>를 반환합니다.

초기값이 있는 reduce()를 사용하면 fold()와 같은 동작을 합니다.


정리

특징 reduce() fold()
초기값 사용 불가 ❌ (첫 번째 요소가 초기값) 사용 가능 (명시적으로 지정)
빈 컬렉션 처리 예외 발생 ❌ (UnsupportedOperationException) 초기값 그대로 반환
첫 번째 요소 첫 번째 요소가 acc, 두 번째 요소부터 연산 초기값이 acc, 첫 번째 요소부터 연산
Java Stream API 대응 reduce() (초기값 없는 버전) reduce() (초기값 있는 버전)

 

일반적으로 fold()가 더 유연하고 안전한 선택지입니다.

빈 Collection을 처리해야 하거나 초기값이 필요한 경우 fold()를 사용하는 것이 좋습니다.

 

하지만, 컬렉션의 첫 번째 요소를 초기값으로 사용하고 싶다면 reduce()를 선택할 수 있습니다.