본문 바로가기

코틀린

코틀린: fold와 reduce - 누적 합과 누적 곱

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 }
}