Published on

Kotlin IN ACTION | 고차 함수와 타입 파라미터

Authors
  • avatar
    Name
    이건창
    Twitter

Introduction

8장 : 고차 함수

고차 함순느 람다를 인자로 받거나 반환하는 함수를 말해. 고차 함수를 활용하면 코드 중복을 없애고 높은 추상화를 추구 할 수 있지. 그리고 람다로 인한 성능상 부가 비용을 줄여 유연하게 흐름을 제어 할 수 있는 인라인 함수도 존재해.

고차 함수

초차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수를 말 해.

함수를 인자로 받는 고차 함수

인자로 받는 경우 다음처럼 구현 할 수 있어.

val sum1 = { x: Int, y: Int -> x + y }
val sum2: (Int, Int) -> Int = { x, y -> x + y }

인자로 받을 때 주의사항은 반환 값이 null 일 수 있고, 함수 타입이 null 일 수 있어. 반환 값이 null 인 경우는 다음과 같아.

val returnNull = { x: Int, y: Int -> null }

함수 타입이 null인 건 다음처럼 표시 해.

val funNull: ((Int, Int) -> Int)? = null

둘은 차이가 있으니 신중하게 사용해야 해.

인자로 받는 함수는 다음처럼 정의 할 수 있어.

fun function1(/*파라미터 이름*/operation: /*파라미터 함수 타입*/(Int, Int) -> Int) {
    val result = operation(2, 3)
    println("$result")
}

우리는 이전에 디폴트 파라미터를 지정할 수 있음을 배웠어. 함수를 인자로 받을 때에도 활용 가능 해.

fun <T> Collection<T>.joinToString(
    transform: (T) -> String = {it.toString()}
): String {
    val result = StringBuilder()
    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(", ")
        result.append(transform(element))
    }
    return result.toString()
}

함수를 반환하는 고차 함수

프로그램 상태나 조건에 따라 달라질 수 있는 로직이 있을 경우 함수를 반환하는 게 좋은 방법일 수 있어. 작성하는 방법은 반환 받은 함수를 변수에 저장하고 변수를 사용 할 때 인자를 추가하면 돼.

fun getStringToDouble(): (String) -> Double {
    return { it.toDouble() }
}

val double = getStringToDouble()
println("${double("123.0")}")

인라인 함수

인라인 함수는 컴파일 타임에 바이트 코드로 호출한 함수에 추가하는 방식을 말해. 덕분에 람다가 실행될 때마다 발생하는 무명 클래스 생성 비용을 줄일 수 있어.

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

인라인 함수 작동하는 방식

다음과 같은 상황에서 인라인 키워드를 적용하지 않을 경우와 적용할 경우를 비교했어.

fun foo(l: Lock) {
    println("Before")
    synchronized(l) {
        println("Hi")
    }
    println("After")
}

인라인 키워드를 적용하지 않는 경우에는 InlineHelloKt 클래스 인스턴스를 생성하고 메서드를 호출하는 모습을 볼 수 있어.

L2
LINENUMBER 10 L2
ALOAD 0
GETSTATIC MainKt$foo$1.INSTANCE : LMainKt$foo$1;
CHECKCAST kotlin/jvm/functions/Function0
INVOKESTATIC InlineHelloKt.synchronized (Ljava/util/concurrent/locks/Lock;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
POP

인라인 키워드를 적용한 경우는 다음 내부에 메서드 호출 없이 synchronized 내부에 존재하는 메서드가 호출 된 모습을 볼 수 있어.

L2
LINENUMBER 20 L2
FRAME FULL [java/util/concurrent/locks/Lock I] [java/lang/Throwable]
ASTORE 2

쉽게 성능 튜닝하기 어려운데 인라인을 활용하면 쉽게 가능하기 때문에 적극적으로 사용하면 좋아보여. 하지만 인라인 함수는 제약이 많아.

인라인 함수 본문에서 람다 식을 바로 호출하거나 람다 식을 인자로 전달받아 바로 호출하는 경우에만 인라이닌을 사용 할 수 있어. 만약 인라인 함수 본문에서 변수로 저장한다면 코틀린은 인라이닌을 어떻게 할지 어려워 해.

즉, 이런식으로는 힘들다는거지.

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        val action1 = action // Illegal usage of inline-parameter 'action' !!
        return action()
    } finally {
        lock.unlock()
    }
}

이런 경우 noinline 키워드를 추가하거나 inline 키워드를 지워야 해.

고차 함수 안에서 흐름 제어

람다는 논 로컬 리턴이다.

보통 루프문에서 return을 수행한다면 바깥에 있는 블록을 반환하게 돼. 이런 걸 non-local return 이라고 해. 코틀린은 레이블을 지정하면 루프문에서 local return 할 수 있어.

fun look(people: List<String>) {
    people.forEach label@/*lambda 레이블*/{
        if (it.startsWith("hello")) return@label/*return expression 레이블*/
    }
}

아니면 메서드 이름을 레이블로 지정 할 수 있지.

fun look(people: List<String>) {
    people.forEach {
        if (it.startsWith("hello")) return@forEach
    }
}

무명 함수는 로컬 리턴이다.

무명 함수에서 반환문을 작성하면 함수가 닫히기 때문에 local return이야.

fun look(people: List<String>) {
    people.forEach(
        fun(it) {
            if (it.startsWith("hello")) return
        }
    )
}

9장 : 제네릭스

타입 파라미터

코틀린은 제네릭 타입의 인자를 프로그래머가 명시해야 해. 이런 부분이 자바랑 차이가 있어.

list = List() // 안됨

타입 파라미터 제약

타입 파라미터에 제약을 걸고 싶을 때가 있을 수 있어.

fun <T : Number> List<T>.sum() : T

간혹 두 개 이상의 제약이 걸고 싶을 떄가 있어. 이럴 땐 다음처럼 where를 활용해 지정하면 돼.

fun <T> ensure(seq: T) where T : CharSequence, T : Appendable

소거된 타입 파라미터

코틀린도 마찬가지로 런타임에 타입 파라미터 정보가 지워져. 그럼 해당 List와 같이 타입 파라미터를 명시해줘야 하는 친구들의 내부 값을 확인 할 수 없지.

그럴 경우 * 키워드를 사용하면 원하는 제네릭 타입으로 캐스팅 할 수 있도록 도와줘.

fun print(c: Collection<*>) {
    val intList = c as? List<Int> ?: throw IllegalArgumentException()
    println(intList)
}

위 예제에서 리스트가 아닌경우 IllegalArgumentException가 발생하고 내부 데이터가 Int 가 아닌 경우 ClassCastException이 발생해

실체화한 타입 파라미터

타입 파라미터는 실행 시점에 지워져서 입력받는 타입을 확인 할 수 없어. 실제 실행 환경에서 타입을 확인하기 위해서는 inlinereified 키워드를 활용하면 런타임에도 인자를 비교 할 수 있게 돼. 이 함수는 들어온 값이 T 의 인스턴스인지를 비교할 수 있게 도와주는 키워드야.

inline fun <reified T> isT(value: Any) = value is T

filterIsInstance 함수는 위 예제처럼 inlinereified 키워드로 구성되어 있어.

sealed class LottoGame(val numbers: LottoNumbers) {
    class Manual(manualNumbers: LottoNumbers) : LottoGame(manualNumbers)
    class Auto(autoNumbers: LottoNumbers) : LottoGame(autoNumbers)
}

private fun printLottoGames(lottoGames: List<LottoGame>) {
    val manualGames = lottoGames.filterIsInstance<LottoGame.Manual>()
    //...
}

바이트 코드를 살펴보면 함수 호출하는 곳이 없어. L5 에서 hasNext 를 통해 Object를 가져오고 L7에서 LottoGame$Manual 인지를 검사해. 그리고 L8에서 L5로 다시 이동하지.

  private final printLottoGames(Ljava/util/List;)V
   // 어쩌구 저쩌구 초기화
   L5
   FRAME FULL [view/OutputView java/util/List T java/lang/Iterable I java/lang/Iterable java/util/Collection I java/util/Iterator] []
    ALOAD 8
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z (itf)
    IFEQ L6
    ALOAD 8
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object; (itf)
    ASTORE 9
   L7
    ALOAD 9
    INSTANCEOF model/lotto/LottoGame$Manual
    IFEQ L5
    ALOAD 6
    ALOAD 9
    INVOKEINTERFACE java/util/Collection.add (Ljava/lang/Object;)Z (itf)
    POP
   L8
    GOTO L5
   L6
    LINENUMBER 67 L6
   FRAME SAME
    ALOAD 6
   // ...

inline 으로 함수가 삽입된 모습을 볼 수 있고 reified 키워드 덕분에 T 인스턴스 값이 바이트코드에 포함될 수 있지.

공변 무공변

인자의 하위 타입 관계가 타입 파라미터에서도 유지되는지 아닌지에 따라 공변, 반공변, 무공변이라고 해.

하위 타입이 유지되는 걸 공변이라고 해. 그리고 타입 파라미터 앞에 out 키워드를 붙어야 해. 공변임을 표시하면 lower bound가 가능하지.

interface Producer<out T>

반공변인 경우는 하위 타입 관계가 뒤집히게 돼. 그리고 타입 파라미터 앞에 in 키워드를 붙어야 해. 그럴 때 다음처럼 사용하면 upper bound로 지정 돼.

interface Producer<in T>

타입 관계를 지정하지 않으려면 그냥 그대로 사용하면 돼.

interface Producer<T>