어제 공부하다가 끝내지 못했던 컬렉션을 집계하는 방법에 대해 마저 다 알아보았다.
1. fold()와 reduce()
1-1. fold(), reduce()
fold()와 reduce()는 입력받은 연산을 컬렉션 요소에 순차적으로 적용하여 누적된 결과를 반환하는 함수이다. 두 연산 모두 누적된 값과 컬렉션 요소 두 가지의 인자를 받는다.
fold()는 초기 값을 받아 첫 번째 누적 값으로 사용하지만, reduce()는 첫 연산에서 첫 번째 요소와 두 번째 요소를 인수로 사용한다.
reduce()의 경우는 람다의 두 인자 타입이 서로 호환되어야 사용이 가능하다. 따라서 누적 값의 타입이 컬렉션 원소와 다르면 reduce()를 사용해 값을 누적할 수 없다.
fold()의 경우는 누적값의 첫 값이 존재하고 거기에다 컬렉션의 요소를 인자로 받아 새로운 누적값을 만들어주는 형태이기 때문에 누적값의 타입이 컬렉션 요소와 다르게 만드는 것이 가능하다.
fun main() {
val numbers = listOf(11, 22, 33, 44, 55, 66)
println(numbers.reduce { acc, i -> acc + i }) // 231
// acc는 누적 값을, i는 컬렉션의 요소를 나타낸다
// 11+22 -> 33+33 -> 66+44 -> 110+55 -> 165+66 -> 231
println(numbers.fold(0) { acc, i -> acc + i }) // 231
// 0+11 -> 11+22 -> 33+33 -> 66+44 -> 110+55 -> 165+66 -> 231
println(numbers.fold("") { acc, i -> acc + i }) // 112233445566
// 요소가 정수여도 fold()를 사용하여 문자열로 누적이 가능하다
}
1-2. reduceRight()
reduceRight()는 연산의 진행 방향이 왼쪽에서 오른쪽이 아니라 오른쪽에서 왼쪽으로, 마지막 요소에서 부터 첫 번째 요소로 진행된다. 아래 예시를 보면 연산이 어떤 순서로 진행되는지 쉽게 알 수 있다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7)
val strings = listOf("1", "2", "3", "4", "5", "6", "7")
println(strings.reduce { acc, i -> "f($acc, $i)" })
// f(f(f(f(f(f(1, 2), 3), 4), 5), 6), 7)
println(strings.reduceRight { i, acc -> "f($i, $acc)" })
// f(1, f(2, f(3, f(4, f(5, f(6, 7))))))
println(numbers.reduce { acc, i -> acc - i}) // -26
// 1-2 -> -1-3 -> -4-4 -> -8-5 -> -13-6 -> -19-7 -> -26
println(numbers.reduceRight { i, acc -> i - acc }) // 4
// 6-7 -> 5-(-1) -> 4-6 -> 3-(-2) -> 2-5 -> 1-(-3) -> 4
}
1-3. foldRight()
foldRight()역시 fold()와 다르게 연산의 진행 방향이 마지막 요소에서부터 첫 번째 요소로 진행되는 함수이다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7)
println(numbers.fold("0") { acc, i -> "f($acc, $i)" })
// f(f(f(f(f(f(f(0, 1), 2), 3), 4), 5), 6), 7)
// 요소가 정수여도 fold()를 사용하면 누적값을 문자열로 만들 수 있다
println(numbers.foldRight("0") { i, acc -> "f($i, $acc)" })
// f(1, f(2, f(3, f(4, f(5, f(6, f(7, 0)))))))
println(numbers.fold(0) { acc, i -> acc - i}) // -28
// 0-1 -> -1-2 -> -3-3 -> -6-4 -> -10-5 -> -15-6 -> -21-7 -> -28
println(numbers.foldRight(0) { i, acc -> i - acc }) // 4
// 7-0 -> 7-6 -> 5-(-1) -> 4-6 -> 3-(-2) -> 2-5 -> 1-(-3) -> 4
}
1-4. reduceIndexed(), reduceRightIndexed(), foldIndexed(), foldRightIndexed()
집계 연산을 수행할 때 인덱스도 감안해 연산을 수행하고 싶을 때 reduceIndexed(), foldIndexed()를 사용할 수 있다. 매개 변수가 3개인 3항 람다가 함수로 전달된다. 첫 번째 값은 인덱스, 두 번째 값은 누적값, 세 번째 값은 컬렉션의 요소가 들어간다.
fun main() {
val strings = listOf("A", "B", "C", "D", "E")
val numbers = listOf(1, 2, 3, 4, 5, 6, 7)
// reduceIndexed의 경우 첫 번째 요소가 누적값의 시작이 되므로 인덱스가 1부터 시작된다
// 함수마다 연산의 순서를 잘 살펴보자
println(strings.reduceIndexed { index, acc, i -> "[$index]f($acc, $i)" })
// [4]f([3]f([2]f([1]f(A, B), C), D), E)
println(strings.foldIndexed("init") { index, acc, i -> "[$index]f($acc, $i)"})
// [4]f([3]f([2]f([1]f([0]f(init, A), B), C), D), E)
println(strings.reduceRightIndexed { index, i, acc -> "[$index]f($i, $acc)" })
// [0]f(A, [1]f(B, [2]f(C, [3]f(D, E))))
println(strings.foldRightIndexed("init") { index, i, acc -> "[$index]f($i, $acc)"})
// [0]f(A, [1]f(B, [2]f(C, [3]f(D, [4]f(E, init)))))
println(numbers.foldIndexed(0){index, acc, i -> if(index%2 == 0) acc+ i else acc}) // 16
// 인덱스가 0 또는 짝수인 수들의 합: 1+3+5+7 = 16
println(numbers.foldRightIndexed(0){index, i, acc -> if(index%2 != 0) acc+ i*i else acc}) // 56
// 인덱스가 홀수인 수들의 제곱의 합: 6^2+4^2+2^2 = 36+16+4 = 56
}
1-5. reduceOrNull(), reduceRightOrNull(), reduceIndexedOrNull(), reduceRightIndexedOrNull()
reduce()는 첫 연산을 수행할 때 첫 번째 요소를 누적된 결과의 첫 요소로 사용한다. 따라서 빈 컬렉션이 들어오게되면 예외를 발생시킨다. 빈 컬렉션에 대해 reduce()를 적용했을때 null을 반환받고 싶다면 위와 같은 4개의 함수를 사용할 수 있다.
fold()의 경우에는 초기값을 받기 때문에 빈 컬렉션에 fold()를 사용해도 예외가 발생하지 않고 초기값이 반환된다.
reduce()의 경우 컬렉션의 요소가 하나인 경우에는 람다식에 관계없이 그 요소를 그대로 반환한다.
fun main() {
val numbers = listOf(1)
val empty = emptyList<Int>()
println(numbers.reduce { acc, i -> acc*1000 + i*1000 }) // 1
// 컬렉션의 요소가 하나이면 reduce는 그 요소를 그대로 반환한다
println(empty.reduceOrNull { acc, i -> acc}) // null
// 빈 컬렉션에 reduce를 사용하면 예외가 발생한다.
println(empty.reduce { acc, i -> acc})
// UnsupportedOperationException: Empty collection can't be reduced.
}
2. 오늘 알게 된 것
- 여러번 보았지만 제대로 알지 못했던 fold(), reduce()에 대해 자세히 알게 되었다.
- 함수에 대해 정리해보고 값을 이것저것 넣어보는 것만으로도 상당히 많은 공부가 된 것 같다.
'오늘 배운 것' 카테고리의 다른 글
24-03-29 정규 표현식 (0) | 2024.03.29 |
---|---|
24-03-28 NoSQL과 수평 확장성 (0) | 2024.03.28 |
24-03-26 Kotlin 컬렉션 집계하기 (0) | 2024.03.26 |
24-03-25 데이터베이스와 쿼리 (0) | 2024.03.25 |
24-03-24 라이브러리와 프레임워크, 플러그인 (0) | 2024.03.24 |