[Kotlin] 고차 함수 안에서 흐름 제어
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` 뒤에 레이블로 사용해도 된다.
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` 으로 만들지 않은 다른 람다 표현식과 조합해서 사용할 때 문제가 발생하는 경우 활용한다.
- 아규먼트로 `inline` 함수를 받지만, non-local return 을 하는 함수는 받을 수 없게 만든다.
- `noinline`
- 아규먼트로 `inline` 함수를 받을 수 없게 만든다.
`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` 으로 정의된 함수이다.
익명 함수는 일반 함수와 비슷해보이지만, 실제로는 람다 식에 대한 문법적 편의일 뿐이다.
람다 식의 구현 방법이나 람다 식을 인라인 함수에 넘길 때 어떻게 바디가 인라이닝되는지 등의 규칙은 익명 함수에도 모두 적용된다.