Inline
코틀린에서 higher-order function에 사용되는 lambda 표현식은 새로운 함수객체를 생성하게 된다. 일상적으로 사용되는 lambda 표현식이 매우 간단한 형태임을 생각하면, 매번 사용시 객체를 생성하고 메모리를 할당하는게 상당한 오버헤드임을 알 수 있다. 이러한 오버헤드를 없애기 위해, 함수객체를 생성하지 않고 함수안의 코드를 직접 넣어주도록 하는 키워드가 ‘inline’이다.
간단한 코드를 가지고 확인해보자.
fun highOrderFunc(lambdaFunc: () -> Unit){
lambdaFunc()
}
fun main(args: Array<String>) {
highOrderFunc { println("What happened?") }
}
lambda 함수를 인자로 갖는 간단한 higher-order function을 구현했다. 이를 자바코드로 변환해보면 다음과 같다.
public final class MainKt {
public static final void highOrderFunc(@NotNull Function0 lambdaFunc) {
Intrinsics.checkNotNullParameter(lambdaFunc, "lambdaFunc");
lambdaFunc.invoke();
}
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
highOrderFunc((Function0)null.INSTANCE);
}
}
바이트코드를 거쳐 생성된 자바코드라 조금 복잡해 보이지만, highOrderFunc() 의 인자로 Function0 타입의 객체를 생성하고 이 객체의 invoke() 메소드를 호출해서 실행하는걸 알 수 있다. 그렇다면, inline을 사용시 어떻게 변하는지 살펴보자.
inline fun highOrderFunc(lambdaFunc: () -> Unit){
lambdaFunc()
}
fun main(args: Array<String>) {
highOrderFunc { println("What happened?") }
}
inline 키워드를 함수앞에 붙여줬다. 이걸 자바 코드로 변환해보면,
public final class MainKt {
public static final void highOrderFunc(@NotNull Function0 lambdaFunc) {
int $i$f$highOrderFunc = 0;
Intrinsics.checkNotNullParameter(lambdaFunc, "lambdaFunc");
lambdaFunc.invoke();
}
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
int $i$f$highOrderFunc = false;
int var2 = false;
String var3 = "What happened?";
boolean var4 = false;
System.out.println(var3);
}
}
inline이 붙은 함수를 호출하는 부분에는 함수호출이 없이, 그 함수의 코드가 직접 들어가는걸 확인할 수 있다. inline이라는 키워드 그대로, 라인에 직접 코드가 삽입된다.
noinline
이제 조금 더 깊이 들어가보자. inline이 효율적인건 알게 되었다. 그런데, 다음과 같이 인자로 받은 lambda 함수를 다른 함수의 인자로 넘겨주면 어떻게 될까?
inline fun highOrderFunc(lambdaFunc: () -> Unit){
//lambdaFunc()
receiveLambdaFunc(lambdaFunc)
}
fun receiveLambdaFunc(lambdaFunc: () -> Unit){
println("receiveLambdaFunc")
lambdaFunc()
}
fun main(args: Array<String>) {
highOrderFunc { println("What happened?") }
}
실행하기도 전에 다음과 같은 에러를 맞는다.
Illegal usage of inline-parameter ‘lambdaFunc’ in ‘public inline fun highOrderFunc(lambdaFunc: () -> Unit): Unit defined in root package in file main.kt’. Add ‘noinline’ modifier to the parameter declaration
친절하게도 대처법까지 알려주고 있는데, 바로 ‘noinline’ 키워드를 사용하는 것이다. 다음과 같이 인자에 붙여주면 문제가 해결되는걸 확인 할 수 있다.
inline fun highOrderFunc( noinline lambdaFunc: () -> Unit){
//lambdaFunc()
receiveLambdaFunc(lambdaFunc)
}
non-local control flow
코틀린에서 lambda function에서는 lable을 이용한 return을 제외하곤 return의 사용이 허용되지 않는다. 사용하면, 바로 에러가 잡히는걸 확인할 수 있을 것이다.
var myLambda = {
print("return not allowed")
return // <-- Error
}
흥미롭게도, inline 함수의 인자로 넘어가는 경우엔 return의 사용이 가능하다.
inline fun highOrderFunc( lambdaFunc: () -> Unit){
lambdaFunc()
}
fun main(args: Array<String>) {
highOrderFunc {
println("What happened?")
return // <-- 문제가 없다?!
}
println("Can you see me?")
}
왜 문제가 없는지 inline의 특성을 이해하면 알 수 있다. 컴파일 타임에 코드가 변환되며, lambda 함수가 호출되는게 아니라 직접 삽입되기 때문에 return을 만나게되면, inline 함수를 호출하는 상위함수가 return 되며 실행이 종료된다. 위 코드 마지막에 “Can you see me?”를 출력하게 되어 있지만, main함수가 그전에 return되어 출력되지 않는다.
이와같이 lambda내에 return이 있으나, 이를 감싸는 상위 함수가 종료되는 return을 non-local return 이라고 부른다.
crossinline
만약, inline 함수에서 인자로 받은 lambda를 다른 객체를 만들어 할당 하는 경우를 살펴보자.
inline fun highOrderFunc( lambdaFunc: () -> Unit){
val f = object: Runnable{
override fun run() = lambdaFunc()
}
}
fun main(args: Array<String>) {
highOrderFunc {
println("What happened?")
}
}
실제로는 컴파일 에러가 바로 발생하지만, 어쨌든 코드를 java로 변환해 보면 다음과 같다.
public final class MainKt$main$$inlined$highOrderFunc$1 implements Runnable {
public void run() {
int var1 = false;
String var2 = "What happened?";
boolean var3 = false;
System.out.println(var2);
}
}
...
public final class MainKt {
...
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
int $i$f$highOrderFunc = false;
new MainKt$main$$inlined$highOrderFunc$1();
}
}
inline으로 코드가 삽입됐지만, Runnable을 구현한 객체를 생성하고 있기 때문에, 새로 생성한 객체의 run() 인터페이스에 삽입된걸 볼 수 있다. 만약 return이 들어간다면, run() 함수에 포함되어 run()의 종료를 의미할 것이다. 코틀린에서 inline의 경우에 한해서 lambda에 return을 허용하지만, 이와같이 lambda를 다른 곳에 할당하는 경우, inline이 아닌 경우와 마찬가지로 return의 사용을 못하게 하고 있다. 보다 명확하게 하기위해, 이 때 사용하는 키워드가 ‘crossinline’이다. 다음과 같이, 사용한다.
inline fun highOrderFunc(crossinline lambdaFunc: () -> Unit){
val f = object: Runnable{
override fun run() = lambdaFunc()
}
Thread(f).start()
}
lambda에서 return의 사용을 inline의 특수한 경우에만 허용하는 문제는 매우 혼란스럽게 만든다. 왜 그렇게 만들었을까?
여러가지를 찾아봐도 명확하지는 않아서, 개념적인 문제일거라 생각하며 정리해보면 다음과 같다.
- 첫째, lambda의 특수성을 짚고 넘어가자면, 함수처럼 쓰이지만, 마지막 라인의 값을 돌려주는 expression이다. 코틀린에서 return은 가장 가까운 함수를 빠져나가는 의미만 갖고있고, lambda에는 관여하지 않는다. lambda는 expression이지만, 함수처럼 다른 곳에 인자로 전달도 되고, 할당도 된다. 이런 경우, lambda내에 return문을 사용하면, 실행중에 어떤 함수를 빠져나가게 될지 알 수가 없다. 이것이 lambda에서 return을 금지하는 이유라고 생각한다.
- 둘째, 명시적으로 lable을 표기한 return이 가능한 것도 같은 이유라고 보인다. lable이 붙은 경우는 return으로 빠져나가는게 어떤 함수인지 명확하기 때문이다.
- 셋째, 예외적으로 inline의 경우에는, 코드 변환시 lambda의 존재 자체가 없어지기 때문에, 일반 expression과 동일하게되고, return문의 사용도 명시적으로 어떤 함수가 종료되는지 알 수 있기 때문에 lable을 표기한 return문과 동일하게 허용된다고 생각된다.
- 마지막으로, inline이지만, 다른 곳에 할당하는 경우에는 앞의 기준을 적용하면 return문이 어떤 함수를 종료시킬지 알 수 없기 때문에 금지된다고 생각하면 명확하다.

시작은 crossinline을 몰라서였다. 오래 봐도 이해가 안되었기 때문에 이 기회에 아예 inline 관련 정리를 해봤다. inline과 crossinline은 굉장히 흔히 보이니까 요정도는 알아둘 필요가 있을거 같네.