fold, reduce 함수에 관한 보충 설명입니다. 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 5.13 집계(aggregate)를 참고하기 바랍니다.
코틀린의 표준 라이브러리 함수는 어마무시하게 많습니다. 100% 믿고 사용할 수 있는 라이브러리 함수를 안쓴다는 건 그만큼 손해입니다. 이름만 생소할 뿐 사용 방법은 비교적 간단합니다. 사실 함수 이름도 아무렇게나 붙인 게 아닙니다. 여기서는 누적 합 (accumulated sum) 과 누적 곱 (accumulated product) 을 구하는 데 사용하는 fold와 reduce를 집중적으로 알아보겠습니다.
컬렉션 원소의 누적 합이나 누적 곱을 구해야 할 때 여러분은 어떻게 구현합니까? 대부분 for 문을 사용하겠죠. 예제에서는 누적 곱을 구현하지만, 곱셈 연산 기호(*)를 '+'로 바꾸면 누적 합을 구하는 코드로 간단히 변경할 수 있습니다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val accumulatedProduct = calculateAccumulatedProduct(numbers)
println("누적 곱 = $accumulatedProduct")
}
fun calculateAccumulatedProduct(numbers: List<Int>): Int {
var product = 1
for (number in numbers) {
product *= number
}
return product
}
for 문이 식상하니까 아래처럼 repeat 함수를 사용할 수 있겠죠.
fun main() { ... }
fun calculateAccumulatedProduct(numbers: List<Int>): Int {
var product = 1
repeat (numbers.size) { index ->
product *= numbers[index]
}
return product
}
■ reduce
for나 repeat를 사용해서 구현할 수 있지만, 더 재미있고 강력한 기능의 표준 라이브러리 함수가 있습니다. 바로 fold와 reduce 입니다. 먼저 reduce를 사용해 구현해 볼까요. 아래처럼 단 1줄이면 됩니다. reduce는 람다 식에 2개의 인자(누적값 acc와 원소 number)를 전달받습니다.
fun main() { ... }
fun calculateAccumulatedProduct(numbers: List<Int>): Int {
return numbers.reduce { acc, number -> acc * number }
}
■ runningReduce
더 재미있는 함수는 runnungReduce입니다. runningReduce를 사용하면 단계별 누적 계산 결과를 확인할 수 있습니다. runningReduce 함수의 실행 결과는 리스트 컬렉션(List<Int>)입니다. runningReduce 함수가 반환하는 리스트 컬렉션의 두 번째 원소부터 누적 곱 연산 결과입니다. 최종 누적 곱을 알고 싶으면 마지막 원소를 가져오면 됩니다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val accumulatedProduct = calculateAccumulatedProduct(numbers)
println("단계별 누적 곱 리스트: $accumulatedProduct") // [1, 2, 6, 24, 120]
println("최종 누적 곱 = ${accumulatedProduct.last()}") // 120
}
fun calculateAccumulatedProduct(numbers: List<Int>): List<Int> {
return numbers.runningReduce { acc, n ->
acc * n
}
}
■ 응용 예제: reduce를 사용한 factorial 함수 구현
reduce를 사용한 간단한 응용 예제로 factorial을 구해 보겠습니다. for 문을 사용했을 때와 비교하면 차이를 분명히 알 수 있습니다. 라이브러리 함수 사용은 신뢰성있는 코드로 만들어 줄 뿐만 아니라 가독성(readability)을 높여줍니다.
fun main() {
val number = 5
val factorial = computeFactorial(number)
println("$number! = $factorial")
}
fun computeFactorial(number: Int): Int {
return (1..number).reduce { acc, number -> acc * number }
}
■ fold
reduce로 작성한 코드를 fold를 사용한 코드로 수정해 보면서 두 함수의 차이점을 알아보겠습니다. fold와 reduce는 한 가지만 제외하면 똑같습니다. reduce는 초깃값이 없지만, fold는 실 인자로 초깃값을 지정할 수 있습니다. 아래 예에서 fold(1)의 1이 초깃값입니다. 1(초깃값)*1(첫 번째 원소)을 계산하고, 계산 결과인 1과 두 번째 원소 2를 곱합니다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val accumulatedProduct = calculateAccumulatedProduct(numbers)
println("누적 곱 = $accumulatedProduct")
}
fun calculateAccumulatedProduct(numbers: List<Int>): Int {
return numbers.fold(1) { acc, number ->
acc * number
}
}
fold 함수의 계산 과정을 자세히 들여다볼 수 있는 방법이 있습니다. fold()의 초깃값을 listOf(1)로 수정하고, calculateAccumulatedProduct() 함수의 반환 타입을 Int에서 List<Int>로 수정합니다. 람다 식 블록에 println()을 추가하고, acc + acc.last() * number로 수정합니다. 기호(+)는 plus() 함수를 연산자 오버로딩한 것입니다. 덧셈이 아니라 리스트 컬렉션 객체에 원소를 추가합니다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val accumulatedProduct = calculateAccumulatedProduct(numbers)
println("누적 곱 = $accumulatedProduct")
}
fun calculateAccumulatedProduct(numbers: List<Int>): List<Int> {
return numbers.fold(listOf(1)) { acc, number ->
println("acc = $acc, acc.last() = ${acc.last()}, number = $number")
acc + acc.last() * number // acc.plus( acc.last() * number)
}
}
단계별 실행 결과를 살펴볼까요. 초깃값은 원소가 1인 리스트입니다. 첫 번째 원소(1)와 누적곱(1*1=1)이 리스트 acc 에 추가되어 [1, 1]이 됩니다. 두 번째 원소(2)와 누적곱(1*2=2)이 acc에 추가되어 [1, 1, 2]가 됩니다. 이렇게 쭉~ 계산하면 단계별 누적 곱을 리스트 원소로 반환합니다. 함수의 반환 타입은 List<Int>입니다.
초깃값 1이 불필요하게 리스트의 첫 번째 원소를 차지하고 있죠. 불필요한 첫 번째 원소를 삭제한 리스트 객체를 반환하는 게 좋겠죠. 위 코드를 정리하면 아래와 같습니다. 람다 식 블록 실행을 마친 뒤 drop(1)을 실행하면 됩니다.
fun calculateAccumulatedProduct(numbers: List<Int>): List<Int> {
return numbers.fold(listOf(1)) { acc, number ->
acc.plus( acc.last() * number)
}.drop(1)
}
■ runningFold
위 코드는 runningFold를 사용하면 확~ 코드를 줄일 수 있습니다. runningFold도 runningReduce처럼 단계별 누적 계산 결과를 확인할 수 있습니다
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val accumulatedProduct = calculateAccumulatedProduct(numbers)
println("단계별 누적 곱 = $accumulatedProduct")
}
fun calculateAccumulatedProduct(numbers: List<Int>): List<Int> {
return numbers.runningFold(1) { acc, number ->
acc * number
}.drop(1)
}
■ 응용 예제: fold를 사용한 factorial 함수 구현
이번에는 reduce 대신 fold를 사용해 factorial을 구해 보겠습니다.
fun main() {
val number = 5
val factorial = computeFactorial(number)
println("$number! = $factorial")
}
fun computeFactorial(number: Int): Int {
return (1..number).fold(1) { acc, n -> acc * n }
}
'코틀린' 카테고리의 다른 글
코틀린: 시퀀스(Sequence)와 컬렉션(Collection)은 뭐가 다를까요? (0) | 2025.01.17 |
---|---|
코틀린: Comparable 인터페이스와 Comparator 인터페이스 (0) | 2025.01.17 |
코틀린: 한 걸음 뒤에서 컬렉션의 숲을 바라볼까요? (0) | 2025.01.17 |
코틀린: 4장 테스트에 도전해 보세요(3,4,5번 정답 해설) (0) | 2025.01.17 |
코틀린: 4장 테스트에 도전해 보세요 (0) | 2025.01.17 |