[코틀린] 고차 함수 흐름 제어

hee09·2021년 12월 22일
0
post-thumbnail

for문과 같은 루프의 중간에 있는 return문의 의미는 이해하기가 쉽습니다. 그런데 그 루프를 filter와 같이 람다를 호출하는 함수로 바꾸고 인자로 전달하는 람다 안에서 return을 사용하면 어떤 일이 벌어질까요? 예제를 통해 알아보겠습니다.

람다 안의 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을 사용해 정의되므로 자기 자신이 가장 안쪽에 있기에 자신을 반환시키는 것입니다.

무명함수와 람다

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

참조
Kotlin in action

틀린 부분은 댓글로 남겨주시면 수정하겠습니다..!!

profile
되새기기 위해 기록

0개의 댓글