Kotlin

[Kotlin] 고차 함수 안에서 흐름 제어

sh1mj1 2024. 1. 30. 17:22

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

filter 와 함께 람다 안에서 return 을 사용하는 등의 예제를 살펴보자.

 

람다 안의 return 문: 람다를 둘러싼 함수로부터 리턴(non-local return)

일반 루프 안에서 `return` 사용하기

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

fun lookForAlice(people: List<Person>) {
    for (person in people) {
        if (person.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

val people = listOf(Person("Alice", 29), Person("Bob", 31))
lookForAlice(people) // print /* Found! */

 

`forEach` 에 전달된 람다에서 `return` 사용하기

fun lookForAlice2(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

lookForAlice2(people) // print /* Found! */

 람다 안에서 `return` 을 사용하면, 람다로부터만 리턴되는 것이 아니고, 람다를 호출하는 함수가 실행을 끝내고 리턴된다.

이렇게 자신을 둘러싼 블록보다 더 바깥에 있는 다른 블록을 리턴하게 만드는 return 문non-local(넌로컬) return 이라고 부른다.

 

Java 메서드 내의 `for` 루프나 `synchronized` 블록에서 `return` 키워드를 사용하는 경우에도 루프나 블록에서 리턴되는 것이 아닌, 함수 전체에서 리턴된다.

코틀린에서는 이러한 동작을 함수에서 람다를 인자로 받는 경우에도 똑같이 동작한다.

하지만 람다를 받는 함수가 inline 함수 일 때만 non-local return 이 가능하다.

@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

 예를 들어 `forEach` 함수의 바디가 인라인 함수로 처리되면 람다의 바디도 함께 인라인되므로, 람다 내의 `return` 표현식이 non-local return 이 가능하다.

 

하지만 non-inline 함수에서는 람다에 대한 리턴이 되지 않는다.

non-inline 함수는 람다를 변수에 저장해 둔 후에 람다가 호출될 수도 있다. 

그런 경우 함수가 이미 리턴된 후에 람다가 실행되기 때문에 외부 함수의 리턴에 영향을 미칠 수 없다.

// 인라인 함수 예제
inline fun inlineExample(action: () -> Unit) {
    println("Before Lambda")
    action()
    println("After Lambda")
}

// 비인라인 함수 예제
fun nonInlineExample(action: () -> Unit) {
    println("Before Lambda")
    action()
    println("After Lambda")
}

@Test
fun inlineFunctionLambdaReturn() {
    // 인라인 함수 저장 -> 호출
    val delayedLambda = inlineExample {
        println("Inside Lambda")
        return // / 인라인 함수 내에서는 람다의 return이 외부 함수에 영향
    }
    println("after save lambda")
    return delayedLambda
}
// print
/*
Before Lambda
Inside Lambda
 */

@Test
fun non_inlineFunctionLambdaReturn() {
    // 비인라인 함수 저장
    val delayedLambda = nonInlineExample {
        println("Inside Lambda")
        // return  // 주석 해제 시  [COMPILE ERROR] 'return' is not allowed here
    }
    println("after save lambda")
    return delayedLambda
}
// print
/*
Before Lambda
Inside Lambda
After Lambda
after save lambda
 */

 왜 non-inline 함수에서는 람다 내부에서 리턴을 사용할 수 없을까?

함수가 컴파일될 때, 람다 함수는 객체로 래핑되어서 생기기 때문이다. 

람다 함수는 내부적으로 `FunctionN` 와 같은 클래스로 래핑되어서 처리된다. 

이 래핑된 객체 안에서의 `return` 은 해당 객체만을 리턴하기 때문에 외부 함수로의 non-local return 이 불가능하다. 

하지만 인라인 함수에서는 람다 함수가 직접 호출부로 복사되므로 non-local return 이 가능하다.

람다로부터 리턴: 레이블을 사용한 return(local-return)

람다 안에서 람다만을 리턴하는 것은 만들 수 없을까? 이것도 가능하다.

람다 식에서도 local return 을 사용할 수 있다. 

 

람다 안에서 local return 은 `for` 루프의 `break` 와 비슷한 역할을 한다.

local return 은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어나간다.

local return 과 non-local return 을 구분하기 위해 `label` 을 사용해야 한다.

`return` 으로 실행을 끝내고 싶은 람다 식 앞에 레이블을 붙이고 `return` 키워드 뒤에 그 레이블을 추가하면 된다.

fun lookForAlice3(people: List<Person>) {
    people.forEach label@{ // 람다 식 앞에 레이블을 붙인다.
        if (it.name == "Alice") return@label // 앞에서 정의한 레이블을 참조
    }
    println("Alice might be somewhere") // 항상 이 줄이 출력됨
}

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

lookForAlice3(people)
// print /* Alice might be somewhere */

람다에 레이블을 붙이거나 return 뒤에 레이블을 붙이기 위해 @ 를 사용

람다를 인자로 받는 인라인 함수의 이름을 `return` 뒤에 레이블로 사용해도 된다.

fun lookForAlice4(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") return@forEach // return@forEach 는 람다식으로부터 리턴시킴
    }
    println("Alice might be somewhere")
}

lookForAlice4(people)
// print /* Alice might be somewhere */

 람다 식에 레이블을 명시하면 람다 함수 이름을 레이블로 사용할 수는 없다.

람다 식에는 레이블이 2개 이상 붙을 수 없다.

레이블이 붙인 this 식

`this` 식의 레이블에도 같은 규칙이 적용된다.

수신 객체 지정 람다(appy, also 등..) 글에서 이에 대해 잠깐 언급했다.

수신 객체 지정 람다의 바디에서는 `this` 참조를 사용해서 묵시적 컨텍스트 객체(람다를 만들 때 지정한 수신 객체)를 가리킬 수 있다.

assert(
    StringBuilder().apply sb@{ // this@sb 를 통해 이 람다의 묵시적 수신 객체에 접근 가능
        listOf(1, 2, 3).apply { // this 는 이 위치를 둘러싼 가장 안쪽 영역의 묵시적 수신 객체를 가리킨다.
            this@sb.append(this.toString()) // 모든 묵시적 수신 객체를 사용할 수 있다. 다만 바깥쪽 묵시적 수신 객체에 접근할 때는 레이블을 명시해야 한다.
        }
    }
        .toString() == "[1, 2, 3]")

 

 레이블 붙은 `return` 과 마찬가지로 이 경우에도 람다 앞에 명시한 레이블을 사용하거나, 람다를 인자로 받는 함수 이름을 사용할 수 있다.

corssinline & noinline

함수를 인라인으로 만들고 싶지만, 어떤 이유로 일부 함수 타입 파라미터는 `inline` 으로 받고 싶지 않은 경우가 있을 수 있다. 

이러한 경우에는 아래와 같은 한정자를 사용한다.

  • `crossinline`
    • 아규먼트로 `inline` 함수를 받지만, non-local return 을 하는 함수는 받을 수 없게 만든다.
      `inline` 으로 만들지 않은 다른 람다 표현식과 조합해서 사용할 때 문제가 발생하는 경우 활용한다.
  • `noinline`
    • 아규먼트로 `inline` 함수를 받을 수 없게 만든다.
      `inline` 함수가 아닌 함수를 아규먼트로 사용하고 싶을 때 활용한다.
inline fun requestNewToken(
    hasToken: Boolean,
    crossinline onRefresh: () -> Unit,
    noinline onGenerate: () -> Unit
) {
    if (hasToken) {
        httpCall("get-token", onGenerate)
        // 인라인이 아닌 함수를 아규먼트로 함수에 전달하려면 noinline 을 사용
    } else {
        httpCall("refresh-token"){
            onRefresh()
            // Non-local 리턴이 허용되지 않는 컨텍스트에서
            // inline 함수를 사용하고 싶다면 crossinline 을 사용.
            onGenerate()
        }
        
    }
}

fun httpCall(url: String, callback: () -> Unit) {
    /* ... */
}

익명 함수: 기본적으로 local return

위에서 non-local 과 local return 을 알아봤다. 

non-local return 문은 장황하고, 람다가 여러 return 문을 가지면, 사용하기 불편해진다.

코틀린의 익명 함수는 코드 블록을 여기저기 전달하기 위한 다른 해법을 제공하며, 그 해법을 사용하면 non-local return 문을 여럿 사용해야 하는 코드 블록을 쉽게 작성할 수 있다.

 

익명 함수 안에서 `return` 사용하기

fun lookForAlice5(people: List<Person>) {
    people.forEach(fun(person) { // 람다 식 대신 익명 함수를 사용한다
        if (person.name == "Alice") return // return 은 가장 가까운 함수를 가리킨다 이 위치에서 가장 가까운 함수는 익명 함수이다.
        println("${person.name} is not Alice")
    })
}
private val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Andrew", 33))

lookForAlice5(people)
// print
/*
Bob is not Alice
Andrew is not Alice
*/

 익명 함수는 일반 함수와 비슷하다.

차이는 함수 이름이나 파라미터 타입을 생략할 수 있다는 점 뿐이다.

 

`filter` 에 익명 함수 넘기기

people.filter(fun(person): Boolean {
    return person.age < 30
})

people.filter(fun(person) = person.age < 30)

 익명 함수도 일반 함수와 같은 리턴 타입 지정 규칙을 따른다.

블록이 바디인 익명 함수는 리턴 타입을 명시해야 하고, 식이 바디인(expression-body) 익명 함수의 리턴 타입은 생략할 수 있다.

 

`return` 은 기본적으로  컴파일 코드에서`fun` 키워드를 사용해 정의된 가장 안쪽 함수를 리턴시킨다.

람다는 `fun` 을 사용해 정의되지 않으므로, 람다 바디의 `return` 은 람다 밖의 함수를 리턴시킨다.

익명 함수는 `fun` 을 사용해 정의되므로 그 함수 자신이 바로 가장 안쪽에 있는 `fun` 으로 정의된 함수이다.

return 식은 fun 키워드로 정의된 함수를 리턴한다.

익명 함수는 일반 함수와 비슷해보이지만, 실제로는 람다 식에 대한 문법적 편의일 뿐이다.

람다 식의 구현 방법이나 람다 식을 인라인 함수에 넘길 때 어떻게 바디가 인라이닝되는지 등의 규칙은 익명 함수에도 모두 적용된다.