본문 바로가기

코틀린

[Kotlin In Action] 8장. 고차 함수: 파라미터와 반환 값으로 람다 사용

고차 함수 정의

  • 다른 함수를 인자로 받거나 함수를 반환하는 함수
  • 코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있음

함수 타입

(Int, String) ‐> Unit
(x: Int, y: String) ‐> Unit // 코드의 가독성 위해 파라미터에 이름 지정 가능
  • 함수 파라미터의 타입을 괄호 안에 넣고, 화살표를 넣고, 반환 타입을 지정
  • 반드시 반환 타입을 명시해야 함
  • Unit : 의미있는 값을 반환하지 않는 함수의 반환 타입

인자로 받은 함수 호출

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 } )
}
  • 일반 함수를 호출하는 구문과 같음

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

  • 파라미터를 함수 타입으로 선언할 때도 디폴트 값에 람다를 넣어 정의 가능
fun<T> Collection<T>.joinToString(
    separator: String=",",
    prefix: String="",
    postfix: String=""
): String {
    val result = StringBuilder(prefix)
    for((index, element) in this.withIndex()){
        if(index > 0) result.append(separator)
        result.append(element)
    }
    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 +1.2 * order.itemCount}
    }
    return {order -> 1.2 * order.itemCount }
}
  • 위 예시는 Order를 받아 Double을 반환하는 함수

람다를 활용한 중복 제거

  • 코드의 중복을 제거하고 싶을 경우 고차 함수를 사용할 수 있음
val averageMobileDuration = log
    .filter { it.os in setOf(OS.IOS, OS.ANDROID) }
    .map (SiteVisit::duration)
    .average()
    
// 고차 함수를 사용해 중복 제거
fun List<SiteVisit>.averageDurationFor(
    predicate: (SiteVisit) -> Boolean) =
    filter(predicate).map(SiteVisit::duration).average()

인라인 함수: 람다의 부가 비용 없애기

  • 람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생성되어 비용 발생
  • inline 변경자가 붙은 함수는 컴파일러가 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트 코드로 변환

인라이닝이 작동하는 방식

inline fun <T> synchronized(lock : Lock, action: () -> T): T{
    lock.lock()
    try {
        return action()
    }finally {
        lock.unlock()
    }
}
fun foo(l : Lock){
    println("before sync")
    synchronized(l){
        println("action")
    }
    println("after sync")
}


// 컴파일
fun __foo__ (l : Lock){
    println("before sync")
    l.lock()
    try {
        println("action")
    }finally {
        l.unlock()
    }
    println("after sync")
}
  • inline으로 선언한 함수
  • 해당 함수의 본문이 호출 위치에 인라인
  • 호출시 전달한 람다도 인라이닝 됨
  • 한 인라인 함수를 여러 곳에서 각각 다른 람다를 사용해 호출하면 각각 따로 인라이닝 됨

인라인 한계

  • 인자로 전달된 람다식의 본문은 결과 코드에 직접 들어가기 때문에 방식이 한정적
  • 함수 본문에서 파라미터로 받은 람다를 호출하면 쉽게 호출을 람다 본문으로 변경 가능
  • 람다를 다른 변수에 저장 후 나중에 그 변수를 사용하면 람다를 인라인 할 수 없음
    • 람다를 표현하는 객체가 어딘가는 존재해야하기 때문
inline fun <T> synchronized(lock: Lock, action: () ‐> T): T {
    ...
    val someA = action // ‐> 컴파일 에러
    ...
}
Illegal usage of inline‐parameter 'action' ...

인라인 제외

  • 함수 파라미터를 인라인에서 제외하려면 noinline 변경자를 붙여 인라이닝 금지시킴
inline fun foo(f: () ‐> Unit, noinline g: () ‐> Unit) {

}

컬렉션 연산 인라이닝

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

>>> println(people.filter { it.age < 30 })
[Person(name=Alice, age=29)]
  • 컬렉션의 filter, map 등 함수도 인라인 함수
  • 인라인되므로 중간 리스트를 사용 
  • 때문에 부가비용 발생
  • asSequence 사용시 시퀀스로 사용하여 중간 컬렉션을 생성하지 않을 수 있음
  • 시퀀스 연산에서는 람다가 인라이닝 되지 않음
    • 때문에 컬렉션 크기가 작은 경우는 시퀀스보다 컬렉션 연산이 더 성능 좋을 수 있음

함수를 인라인으로 선언해야 하는 이유

  • 일반 함수 호출은 JVM이 이미 강력하게 인라인 지원
    • 바이트 코드를 기계어 코드로 번역하는 JIT에서 발생
    • 바이트 코드에서는 각 함수 구현이 1번만 있으면 되는데
    • 코틀린 인라인 함수는 중복 발생

람다를 인자로 받는 함수를 인라인 할 경우 이익

  • 부가 비용 감소
    • 람다 호출 비용 감소
    • 람다 위한 객체 생성 감소
  • 현재 JVM은 함수 호출과 람다를 인라인할 만큼 똑똑하지 못함
  • 인라이닝은 바이트코드 크기를 증가시키므로 인라인 함수 크기가 작아야 함

자원 관리를 위해 인라인된 람다 사용

use

fun readFirstLineFromFile(path: String): String {
    BufferedReader(FileReader(path)).use { br ‐>
        return br.readLine()
    }
}
  • Closeable에 대한 확장 함수로 자바의 try‐with‐resource와 같은 기능

고차 함수 안에서 흐름 제어

람다 안의 return문: 람다를 둘러싼 함수로부터 반환

fun lookForAlice(people: List<Person>) {
    people.forEach { // forEach : 인라인 함수
        if (it.name == "Alice") {
            println("Found!")
            return // lookForAlice 함수에서 리턴
        }
    }
    println("Alice is not found")
}
  • 람다 안의 return 문은 람다를 호출 하는 함수에서 return
  • non-local return : 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return

람다로부터 반환 : 레이블을 사용한 return

  • 람다식에서 로컬 return 사용 가능
  • for 루프의 break와 비슷
  • 로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드 실행을 지속
  • 로컬 return과 넌 로컬 return을 구분하기 위해 label이나 함수 이름을 label로 사용
fun lookForAlice(people: List<Person>) {
    people.forEach label@ { // @으로 레이블 지정
        if (it.name == "Alice") return@label // 레이블 사용해서 람다에서 리턴
    }
    println("Alice might be somewhere")
}
fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") return@forEach // 함수이름을 레이블로 사용
    }
    println("Alice might be somewhere")
}

무명 함수 : 기본적으로 로컬 return

// 무명 함수
fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (it.name == "Alice") return // 가장 가까운 무명 함수에서 리턴
        println("${person.name} is not Alice")
    })
}
  • return은 가장 가까운 fun 키워드를 사용해 정의된 가장 안쪽 함수를 리턴