High Order Functions

HJ·2022년 5월 4일
0

kotlin in action

목록 보기
1/2

kotlin in action 책의 8장을 정리한 내용 입니다.

High Order Functions(고차함수)

매개변수나 인자, 반환 값에 함수를 이용할 수 있는 것을 말합니다.
람다식과 고차함수를 사용하면 다양한 함수 조합을 사용할 수 있어 생산성이 높아집니다.

// 함수를 매개변수로 사용하기
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {  // 매개변수를 3가지 받는데 x, y, operation는 람다식 함수
    return operation(x, y)                                          // operation 매개변수를 반환하는데, 자체가 람다식
}
 
fun main() {
    val mulResult = calculate(4, 5) { a, b -> a * b }                 
    val sumResult = calculate(4, 5) { a, b -> a + b }                  
}
// 함수를 반환값으로 사용하기
fun operation(): (Int) -> Int {                                    // Int 형을 받고, Int 형을 리턴하는 람다식
    return ::square                                                // 최상의 함수를 참조, 반환 값으로 함수를 이용
}
 
fun square(x: Int) = x * x
 
fun main() {
    val func = operation()
    println(func(2))
}

Function Types

함수를 파라미터로 받기 위해서 Function Types은 아래와 같이 작성합니다.

함수 파라미터의 타입을 괄호 안에 넣고,
그 뒤에 화살표를 추가한 다음, 함수의 반환 타입을 지정합니다.

반드시 반환 타입을 명시해야 합니다.

함수 타입으로 선언을 하면, 람다식의 파라미터 타입을 유추할 수 있어 람다식 안에서 파라미터 타입을 생략이 가능합니다.

// 람다를 변수에 대입 -> sum, action이 함수 타입임을 추론(함수 타입이 생략된 경우)
val sum = { x: Int, Y: Int -> x + y }
val action = { println(42) }
 
// 함수 타입으로 작성
val sum: ( Int, Int ) -> Int = { x, y -> x + y }         // 람다식의 매개변수에는 굳이 타입을 적을 필요가 없음
val action: () -> Unit = { println(42) }                 // 반환타입을 꼭 명시, Unit 생략하면 안돼!
var canReturnNull: (Int, Int) -> Int? = { x, y -> null } // 반환타입에 null이 가능, x와 y를 받으면 무조건 null을 반환
var funOrNull: ((Int, Int) -> Int)? = null               // 함수 타입 전체가 null이 될 수 있는 타입
 
 
// 함수 타입에서 파라미터 이름을 지정할 수 있음
fun performRequest(
    url: String,
    callback: (code: Int, content: String) -> Unit      // 파라미터 이름을 지정이 가능
) {
    /*...*/
}
 
fun main(args: Array<String>) {
    val url = "http://kotl.in"
    performRequest(url) { code, page -> /*...*/ }      // 원하는 이름을 그냥 사용해도 됨
    performRequest(url) { code, content -> /*...*/ }   // 동일한 이름을 사용하는 것이 가독성이 좋아짐(★)
}

인자로 받은 함수를 호출하는 법

함수 이름 뒤에 괄호를 붙이고 괄호 안에 원하는 인자를 콤마(,)로 구분하여 넣는 방식으로 일반 함수를 호출하는 것 같습니다.

fun twoAndThree(operation: (Int, Int) -> Int) {            // 인자로 함수 타입 파라미터를 받겠다고 선언
    val result = operation(2, 3)                           // 일반 함수를 호출하는 것 같쥬~
    println("The result is $result")
}
 
fun main() {
    twoAndThree {  a, b -> a + b }
    twoAndThree {  a, b -> a * b }
}

String의 filter 구현을 다시 자세히 보면

predicate는 Char 형을 받아서, Boolean을 리턴하는 람다식이고
filter 자체의 반환 값은 String 입니다.

자바에서 코트린 함수 타입을 사용하는 법

코틀린의 함수 타입은 자바 코드로 변환 시 FunctionN 인터페이스, invoke 메소드, Unit 타입의 반환 등의 규칙이 있습니다.
자바 코드에서 코틀린 코드를 호출하려면 이런 걸 알아두어야 합니다.

함수 타입은 FunctionN 인터페이스를 구현한 객체로 컴파일 됩니다.
코틀린 표준 라이브러리 함수 인자의 개수에 따라서 FunctionN 인터페이스 제공합니다.
인터페이스의 invoke 메소드가 있어서 invoke를 호출하면 함수 실행하게 되지요.

fun processTheAnswer(f: (Int) -> Int) {
    println(f(42))
}
 
fun main() {
    processTheAnswer { number -> number + 1 }
}

위의 코드를 컴파일된 자바 소스를 보면 위의 설명된 내용의 코드를 볼 수 있습니다.

자바에서 코틀린 코드의 호출이 가능한데 자바의 버전에 따라 호출하는 방법이 다릅니다.
자바 8이후의 버전은 람다를 넘겨 호출이 가능 합니다.

자바 8이후의 버전은 무명클래스를 넘겨 호출이 가능 합니다.
즉 위에서 설명드린 functionN<N..> 인터페이스와 invoke 메소드 등의 개념을 알아야 호출할 수 있는 것이지요.

(참고로 FunctionN 인터페이스는 22개 arguments까지 지원합니다.)

Default 값 지정 및 Null이 될 수 있는 Funcion Types parameter

함수 타입 파라미터에 Default 값을 넣을 수도 있고, Null이 될 수 있는 함수 타입도 넣을 수 있습니다.

1) 함수 타입에 Default 값을 지정하기

선언 방법은 = 뒤에 람다식을 넣으면 됩니다.

package study.chap08
 
import java.util.*
 
fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = "",
    transform: (T) -> String = { it.toString() }                     // function type 파라미터를 받으면서, 디폴트 값으로 람다식을 적어둠
): 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.lowercase(Locale.getDefault()) })            // 결과 값: alpha, beta
    println(letters.joinToString(separator = "! ", postfix = "! ",                 // 결과 값: ALPHA! BETA!
        transform = { it.uppercase(Locale.getDefault()) }))
}

2) Null이 될 수 있는 함수 타입 사용 가능

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = "",
    transform: ((T) -> String)? = null                                    // 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()         // invoke 메소드를 호출 전에 safe-call 이용, 엘비스 연산자로 인자를 받지 않은 경우를 처리
        result.append(str)
    }
 
    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.lowercase(Locale.getDefault()) })   // 결과 값: alpha, beta
    println(letters.joinToString(separator = "! ", postfix = "! ",        // 결과 값: ALPHA! BETA!
        transform = { it.toUpperCase() }))
}

함수를 함수에서 반환

함수를 반환할 때 반환하는 함수의 타입을 지정하고, return 문에는 람다식 같은 것을 넣어 반환하자
1) (반환 타입 선언) 함수 타입을 지정
2) (return 문) 람다나 멤버 참조, 함수 타입의 값을 계산하는 식 등을 넣어 반환

enum class Delivery { STANDARD, EXPEDITED }
 
class Order(val itemCount: Int)
 
fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {                       // Order를 받고 Double 타입의 값을 리턴하는 함수 타입 선언, 1번을 설명(★)
    if (delivery == Delivery.EXPEDITED) {
        return { order -> 6 + 2.1 * order.itemCount }                                        // 람다식을 반환, 2번을 설명(★)
    }
 
    return { order -> 1.2 * order.itemCount }                                                // 람다식을 반환, 2번을 설명(★)
}
 
fun main() {
    val calculator = getShippingCostCalculator(Delivery.EXPEDITED)                           // 반환 받은 함수를 변수에 담았고
    println("Shipping costs ${calculator(Order(3))}")                                        // 일반 함수를 호출하는 것 같쥬~, 결과 값: Shipping costs 12.3
}

람다를 활용한 중복 제거

람다를 통해 중복 제거 및 재활용 하기 좋은 코드를 만들 수 있습니다.

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)
)
 
// Step1. 뽑고자 하는 OS를 직접 기술
val averageWindowsDuration = log
    .filter { it.os == OS.WINDOWS }                                                // 람다식을 필터에 넘김, 다른 운영체제를 뽑으려면 ... 복붙해서 새로운 변수를 하나 더 만들었겠지 ㅋㅋ -> 중복을 피하기 위해 OS를 파라미터로 뽑아보자
    .map(SiteVisit::duration)
    .average()
 
fun main() {
    println(averageWindowsDuration)
}
 
// Step2. OS를 파라미터로 뽑은 것
fun List<SiteVisit>.averageDurationFor(os: OS) =                                   // 중복 코드를 별도 함수로 추출, 가독성이 좋아졌음.
    filter { it.os == os }.map(SiteVisit::duration).average()
 
fun main() {
    println(log.averageDurationFor(OS.WINDOWS))
    println(log.averageDurationFor(OS.MAC))
}
 
// Step3. 갑자기 모바일 디바이스 사용자의 평균 방문 시간을 뽑고 싶어졌어요. ㅠㅠ
val averageMobileDuration = log
    .filter { it.os in setOf(OS.IOS, OS.ANDROID) }                                  // 일단 시간 없으니 하드코딩을 했어요 ㅠㅠ
    .map(SiteVisit::duration)
    .average()
 
fun main(args: Array<String>) {
    println(averageMobileDuration)
}
 
// Step4. 고차함수를 이용해서 중복을 제거해보자
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =         // function types의 predicate를 받아서 하드코딩을 없애자
    filter(predicate).map(SiteVisit::duration).average()                            // 코드의 일부를 복붙하고 싶은 경우가 있다면 그 코드를 람다로 만들어 중복을 제거할 수 있음(★)
 
fun main() {
    println(log.averageDurationFor {
        it.os in setOf(OS.ANDROID, OS.IOS) })
    println(log.averageDurationFor {
        it.os == OS.IOS && it.path == "/signup" })
}
profile
Fake it 'till you become it

0개의 댓글