ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Koltin in Action: ch 8. 고차 함수 : 파라미터와 반환 값으로 람다 사용 (1)
    Book/Kotlin in Action 2023. 1. 8. 23:59

     

    8.1 고차 함수 정의

    고차 함수란? 

    고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수다. 코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있다. 따라서 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수이다.

     

    함수 타입

    함수 타입을 정의하려면 함수 파라미터의 타입을 괄호 안에 넣고, 그 뒤에 화살표(->)를 추가한 다음, 함수의 반환 타입을 지정하면 된다.

    Unit 타입은 의미 있는 값을 반환하지 않는 함수 반환 타입에 쓰는 특별한 타입이다. 그냥 함수를 정의한다면 함수의 파라미터 목록 뒤에 오는 Unit 변환 타입 지정을 생략해도 되지만 함수 타입을 선언할 때는 반환 타입을 반드시 명시해야하므로 Unit을 빼먹어서는 안된다.

     

    예를 들기 위해 코틀린의 maxOf 함수의 공식문서를 살펴보자.

    이 문서를 통해 코틀린의 maxOf는 매개변수가 하나 있는 함수라는것을 확인할 수 있고, 그 매개변수의 타입은 (T) -> R 함수 타입인 것을 확인할 수 있다. 즉, maxOf는 함수를 매개변수로 받는 고차 함수이다.

     

    이어서 인자로 받은 함수 호출에 대해서 살펴보자.

    인자로 받은 함수 호출

    인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 같다.

     

    fun twoAndThree(operation: (Int, Int) -> Int) {
        val result = operation(2, 3)
        println("The result is $result")
    }
    
    fun main(args: Array<String>) {
        twoAndThree { a, b -> a + b }
        twoAndThree { a, b -> a * b }
    }

     

    디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터

    파라미터를 함수 타입으로 선언할 때도 디폴트 값을 정할 수 있다.

    fun <T> Collection<T>.joinToString(
            separator: String = ", ",
            prefix: String = "",
            postfix: String = "",
            transform: (T) -> String = { it.toString() } // 함수 타입 파라미터를 선언하면서 람다를 디폴트 값으로 지정한다. 
    ): String {
        val result = StringBuilder(prefix)
    
        for ((index, element) in this.withIndex()) {
            if (index > 0) result.append(separator)
            result.append(transform(element)) // "transform" 파라미터로 받은 함수를 호출한다. 
        }
    
        result.append(postfix)
        return result.toString()
    }
    
    fun main(args: Array<String>) {
        val letters = listOf("Alpha", "Beta")
        println(letters.joinToString()) // 디폴트 변환 함수를 사용한다. -> Alpha, Beta
        println(letters.joinToString { it.toLowerCase() }) // 람다를 인자로 전달한다. -> alpha, beta
        println(letters.joinToString(separator = "! ", postfix = "! ",
               transform = { it.toUpperCase() })) // 이름 붙은 인자 구문을 사용해 람다를 포함하는 여러 인자를 전달한다. -> ALPHA! BETA!
    }

    다른 디폴트 파라미터 값과 마찬가지로 함수 타입에 대한 디폴트 값 선언도 = 뒤에 람다를 넣으면 된다.

    그리고 이를 호출하려면 일반적인 디폴트 파라미터와 마찬가지로 람다를 생략하거나, 인자나 목록 뒤에 람다를 넣거나, 이름 붙은 인자로 전달할 수 있다.

     

    널이 될 수 있는 함수 타입 파라미터

     

    널이 될 수 있는 함수 타입을 사용할 수도 있다. 다만 널이 될 수 있는 함수 타입으로 함수를 받으면 그 함수를 직접 호출할 수 없다. 코틀린은 Null Pointer Error가 발생할 수 있으므로 컴파일을 거부하기 때문이다.

     

    이를 해결하기 위해 null 여부를 명시적으로 검사하는 것이 한 가지 해결 방법이다. 다른 하나는 함수 타입이 invoke 메소드를 구현하는 인터페이스라는 사실을 사용하면 된다. 일반 메소드처럼 invoke도 안전한 구문으로 callback?.invoke()와 같이 호출한다.

     

    /**
     * 널이 될 수 있는 함수 타입 파라미터 사용
     */
    fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        // 널이 될 수 있는 함수 타입의 파라미터를 선언
        transform: ((T) -> String)? = null
    ): String {
        val result = StringBuilder(prefix)
        for((index, element) in this.withIndex()) {
            if(index > 0) result.append(separator)
            // 안전 호출을 사용해 함수를 호출
            val str = transform?.invoke(element)
                ?: element.toString() // 엘비스 연산자를 사용해 람다를 인자로 받지 않은 경우 처리
            result.append(str)
        }
    
        result.append(postfix)
        return result.toString()
    }

    함수를 함수에서 반환

    고차함수에서 함수를 반환하는 예시를 살펴보자.

    그 예시로 프로그램의 상태나 다른 조건에 따라 달라질 수 있는 로직이 있다. 예를 들면 사용자가 선택한 배송 수단에 따라 배송비를 계산하는 방법이 달라질 수 있다. 이럴 때 적절한 로직을 선택해서 함수로 반환하는 함수를 정의해 사용할 수 있다.

    enum class Delivery { STANDARD, EXPEDITED }
    
    class Order(val itemCount: Int)
    
    fun getShippingCostCalculator(
        delivery: Delivery
    ): (Order) -> Double { // 반환 타입으로 함수 타입을 선언해 함수를 반환하는 함수를 선언
        if(delivery == Delivery.EXPEDITED) {
            return { order -> 6 + 2.1 * order.itemCount } // 함수에서 람다를 반환
        }
    
        return { order -> 1.2 * order.itemCount } // 함수에서 람다를 반환
    }
    
    fun main() {
        val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
        println("Shipping costs : ${calculator(Order(3))}")
    }

    다른 함수를 반환하는 함수를 정의하려면 함수 반환 타입으로 함수 타입을 지정해야 한다. 위의 예에서는 Order 객체를 받아서 Double을 반환하는 함수를 반환한다. 즉, 함수를 반환하려면 return 식에 람다나 멤버 참조나 함수 타입의 값들을 계산하는 식 들을 넣으면 된다.

     

     

    람다를 활용한 중복 제거

    함수 타입과 람다식은 재활용하기 좋은 코드를 만들 때 쓸 수 있는 훌륭한 도구다. 웹사이트 방문 기록을 분석하는 예를 살펴보자.

     

    data class SiteVisit(
        val path: String,
        val duration: Double,
        val os: OS
    )
    
    enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
    
    val log = listOf(
        SiteVisit("/", 34.0, OS.WINDOWS),
        SiteVisit("/", 22.0, OS.MAC),
        SiteVisit("/login", 12.0, OS.WINDOWS),
        SiteVisit("/signup", 8.0, OS.IOS),
        SiteVisit("/", 16.3, OS.ANDROID)
    )
    
    val averageWindowsDuration = log
        .filter { it.os == OS.WINDOWS }
        .map(SiteVisit::duration)
        .average()

    이는 함수를 사용하여 일반 함수를 통해 중복을 줄일 수 있다.

    fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
            filter(predicate).map(SiteVisit::duration).average()
    
    fun main(args: Array<String>) {
        println(log.averageDurationFor {
            it.os in setOf(OS.ANDROID, OS.IOS) }) 
        println(log.averageDurationFor {
            it.os == OS.IOS && it.path == "/signup" })
    }
    • 전략 패턴 : 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.
    • it.os in setOf(OS.ANDROID, OS.IOS) 자체를 파라미터로 보내는 것이 전략 패턴 방법이다.
    • 뽑아낼 수 있는 조건이 많고자 할때 조건별로 함수를 만드는 것이 아니라 그 조건 자체를 넘기면서 중복 제거가 된다.
Designed by Tistory.