ABOUT ME

-

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

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

    inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔준다.

    무의미한 객체 생성 예방

    인라인 함수를 사용하면 람다식을 사용했을 때 무의미하게 객체가 생성되는 것을 막을 수 있다. 

    fun nonInlined(block: () -> Unit) {
        block()
    }
    
    fun doSomething() {
        nonInlined { println("do something") }
    }

    nonInlined라는 함수는 고차 함수로 함수 타입을 인자로 받고 있다. 그리고 doSomething()은 noInlined 함수를 호출하는 함수이다. 이러한 코드를 자바로 표현한다면 아래와 같다.

    public void nonInlined(Function0 block) {
        block.invoke();
    }
    
    public void doSomething() {
        noInlined(System.out.println("do something");
    }

    이렇게 표현되는 코드는 실제로 컴파일하면 아래와 같이 변환된다. 이 코드에서의 문제점은 nonInlined의 파라미터로 새로운 객체를 생성하여 넘겨준다는 것이다. 이 객체는 doSomething 함수를 호출할 때마다 새로 만들어진다. 즉, 이와 같이 사용하면 무의미하게 생성되는 객체로 인해 낭비가 생긴다.

    public static final void doSomething() {
        nonInlined(new Function() {
            public final void invoke() {
                System.out.println("do something");
            }
        });
    }

    이러한 문제점을 해결하기 위해서 인라인을 사용하는 것이다. 인라인을 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해준다. 즉, 객체가 항상 새로 생성되는 것이 아니라 해당 함수의 내용을 호출한 함수에 넣는방식으로 컴파일 코드를 작성하게 된다.

    인라인 키워드 사용

    inline fun inlined(block: () -> Unit) {
        block()
    }
    
    fun doSomething() {
        inlined { println("do something") }
    }

    inline 키워드를 함수에 추가하면 이를 컴파일한 코드를 보면 확인할 수 있지만 위와 같이 불필요한 객체를 생성하지 않고 내부에서 사용되는 함수가 호출하는 함수(doSomething)의 내부에 삽입된다.

    public static final void doSomething() {
        System.out.println("do something");
    }

     

    noninline

    둘 이상의 람다를 인자로 받는 함수에서 일부 람다만 인라이닝하고 싶을 때도 있을 수 있다. 예를 들면 어떤 람다에 너무 많은 코드가 들어가거나 어떤 람다에 인라이닝을 하면 안되는 코드가 들어갈 가능성이 있을 때가 있다. 이런 식으로 인라이닝하면 안 되는 파라미터를 받는다면 noinline 변경자를 파라미터 이름 앞에 붙여서 인라이닝을 금지할 수 있다.

    inline fun sample(inlined: () -> Unit, noinline noInlined: () -> Unit) {
        
    }

     

    정리

    기본적으로 일반 함수 호출의 경우에는 JVM이 이미 강력하게 인라이닝을 지원하고 있다. 따라서 일반 함수에는 inline 키워드 추가할 필요가 없다. 

    inline 변경자를 함수에 붙일 때는 코드 크기에 주의해야한다. 인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모드 호출 지점에 복사해 넣고 나면 코드가 전체적으로 아주 커질 수 있다. 그런 경우에는 람다 인자와 무관한 코드를 별도의 비인라인 함수로 빼낼 수도 있다.

     

    추가

    • 인라인 함수가 람다를 인자로 받는 경우(즉, 함수 타입의 파라미터가 있는 경우)에도 디폴트 파라미터를 사용할 수 있다.
    inline fun <E> Iterable<E>.strings(transform: (E) -> String = { it.toString()} ) =
        map { transform(it) }
    
    val defaultStrings = listOf(1,2,3).strings()
    // 결과: [1,2,3]
    val customStrings = listOf(1,2,3).strings { "($it)" }
    // 결과: [(1),(2),(3)]

     

    람다 안의 return문

    아래의 코드는 단지 for 루프안에서 이름이 Alice인 경우에 반환하는 함수인데, 이 경우에 for문 안에서의 return은 lookForAlice 함수로부터 반환된다는 사실을 분명히 알 수 있다.

    for문

    data class Person(val name: String, val age: Int)
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
    fun lookForAlice(people: List<Person>) {
        for (person in people) {
            if(person.name == "Alice") {
                println("Found")
                // lookForAlice 함수를 반환
                return
            }
        }
    
        println("Alice is not found")
    }

    이 코드를 이제 forEach로 변경하고 람다를 넘겨보자.

    forEach + 람다

    fun lookForAlice(people: List<Person>) {
        people.forEach {
            if(it.name == "Alice") {
                println("Found")
                // lookForAlice 함수를 반환
                return
            }
        }
    
        println("Alice is not found")
    }

    람다 안에서 return을 사용하면 람다로부터만 반환되는 게 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환된다. 즉, 위의 for문 예제과 똑같이 실행된다. 코틀린에서는 람다를 받는 함수의 return은 for 루프의 return과 같은 의미를 갖도록 구현했다. 이렇게 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만든 return문을 넌로컬(non-local) return이라 부른다.

    다만 위와 같이 return이 바깥쪽 함수를 반환시킬 수 있는 때는 람다를 인자로 받는 함수가 인라인 함수인 경우뿐이다. 위의 forEach는 인라인 함수이므로 람다 본문과 함께 인라이닝되고, return식이 바깥족 함수를 반환시키도록 쉽게 컴파일할 수 있습니다. 하지만 인라이닝되지 않는 함수에 전달되는 람다에서는 return을 사용할 수는 없다.

     

    레이블을 사용한 return

    람다 식에는 논로컬 return만 사용할 수 있는게 아니라 로컬 return을 사용할 수도 있다. 람다의 로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어간다(for 루프의 break와 비슷). 코틀린에서 람다에서의 로컬 return과 논로컬 return을 구분하기 위해서 레이블을 사용한다. return으로 실행을 끝내고 싶은 람다 식 앞에 레이블을 붙이고, return 키워드 뒤에 그 레이블을 추가하면 된다.

    레이블을 사용(label)

    // 레이블을 통해 로컬 리턴 사용
    fun lookForAlice(people: List<Person>) {
        people.forEach label@{ // 람다 식 앞에 레이블을 붙임
            if(it.name == "Alice") return@label // return@label은 앞에서 정의한 레이블을 참조
        }
    
        // 항상 이 줄은 출력
        println("Alice might be somewhere")
    }

    결과 : "Alice might be somewhere"

    위의 코드는 레이블을 사용해 로컬 리턴을 구현한 것이다. 따라서 Alice를 찾아도 람다의 실행만을 끝내기에 lookForAlice의 실행은 종료되지 않는다. 결과는 lookForAlice의 나머지 부분인 print문에 해당한다.

    람다 식에 레이블을 붙이려면 레이블 이름 뒤에 @ 문자를 추가한 것을 람다를 { 앞에 넣으면 된다. 람다로부터 반환하려면 return 키워드 뒤에 @ 문자와 레이블을 차례로 추가하면 된다.

     

    람다에 레이블을 붙여서 사용하는 대신 람다를 인자로 받는 인라인 함수의 이름을 return 뒤에 레이블로 사용해도 된다.

    인라인 함수의 이름을 레이블로 사용(forEach)

    fun lookForAlice(people: List<Person>) {
        people.forEach {
            // return@forEach는 람다식으로부터 반환시킵니다.
            if(it.name == "Alice") return@forEach
        }
    
        println("Alice might be somewhere")
    }

    주의할 점은 람다 식의 레이블을 명시하면 함수 이름을 레이블로 사용할 수 없고, 람다 식에는 레이블이 2개 이상 붙을 수 없다.

     

    무명 함수: 기본 로컬 return

    본문 여러 곳에서 return을 해야 하는 경우, 람다 식에서 논로컬 반환문을 작성하기는 불편한데, 이 때 무명 함수를 사용하면 간편해진다. 무명 함수는 코드 블록을 함수에 넘길 때 사용할 수 있는 다른 방법이다. 

    무명 함수

    // 무명 함수 안에서 return 사용하기
    fun lookForAlice(people: List<Person>) {
        people.forEach(fun (person) { // 람다 식 대신 무명 함수를 사용
            // return은 가장 가까운 함수를 가리키는데 이 위치에서 가장 가까운 함수는 무명 함수
            if(person.name == "Alice") return
            println("${person.name} is not Alice")
        })
    }

    결과 : "Bob is not Alice"

    위의 코드에서 무명 함수는 forEach안에 선언된 코드(fun (person) ...)이다. 무명 함수는 기본적으로 로컬 return이기에 return이 된다해서 lookForAlice 함수를 반환하는 것이 아닌 자기 자신을 반환시킨다. 그래서 결과가 Alice는 무명 함수 자신이 반환되어 아래의 코드가 실행되지 않고, Bob에서 반환되지 않기에 아래의 코드가 실행된 것이다.

     

    일반 함수와 차이점, 공통점

    무명 함수는 일반 함수의 차이는 함수 이름이나 파라미터를 생략할 수 있다는 점이다. 다만 일반 함수와 같은 반환 타입 지정 규칙을 따른다. 블록이 본문인 무명 함수는 반환 타입을 명시해야 하지만, 식을 본문으로 하는 무명 함수의 반환 타입은 생략할 수 있다.

    // filter에 무명 함수 넘기기
    // 블록이 본문인 함수로 반환 타입 명시
    people.filter(fun (person): Boolean {
        return person.age < 30
    })
    
    // 식이 본문인 무명 함수 사용
    // 반환 타입 생략
    people.filter(fun (person) = person.age < 30)

    무명 함수 안에서 레이블이 붙지 않은 return 식은 무명 함수 자체를 반환시킬 뿐 무명 함수를 둘러싼 다른 함수를 반환시키지 않는다. 사실 return에 적용된 규칙은 단순히 return은 fun 키워드를 사용해 정의된 가장 안쪽 함수를 반환시킨다는 점이다. 따라서 람다 식은 fun을 사용해 정의되지 않아서 본문의 return은 람다 밖의 함수를 반환시키고, 무명 함수는 fun을 사용해 정의되므로 자기 자신이 가장 안쪽에 있기에 자신을 반환시키는 것이다.

    무명함수와 람다

    무명 함수는 일반 함수와 비슷해 보이지만 실제로는 람다 식에 대한 문법적 편의이다. 람다 식의 구현 방법이나 람다 식을 인라인 함수에 넘길 때 어떻게 본문이 인라이닝 되는지 등의 규칙을 무명 함수에도 모두 적용할 수 있다.

Designed by Tistory.