코틀린 언어 정리 3-3

함수형 프로그래밍

인라인 고차함수와 람다

인라인 고차함수

인라인 처리를 하는 고차함수를 말합니다. 인라인 함수는 문법적으로 이 함수를 호출한 곳에 함수 호출 코드 대신 함수 구현 코드를 직접 적용(인라인화)합니다. 범위 함수들은 람다함수를 입력받는 인라인 고차함수인 경우가 대부분이며(인라인 고차함수가 아닌 경우는 본적이 없습니다.), 이 경우 최종적으로 인라인 고차함수를 호출한 곳에 람다함수의 코드가 인라인화 됩니다. 개념적으로는 다음과 같은 절차로 인라인화 된다고 생각하면 됩니다.


1. 범위함수에 입력된 람다함수가 범위함수에 인라인화 됩니다.

2. 람다함수가 인라인화된 범위함수가, 범위함수를 호출한 곳에 인라인화 됩니다.


non-local returns

람다 함수에서는 원래 return을 사용할 수 없지만, 람다함수가 인라인 고차 함수의 매개변수에 입력값으로 사용 되는 경우 return을 사용할 수 있으며 이 return은 인라인 고차함수를 호출하는 함수의 return문으로 처리됩니다.


예제

// 인라인 고차함수

inline fun inlined(block: () -> Unit): Unit {

   return block()

}


fun main(args: Array<String>) {

   println("main start")

   inlined {

       // 인라인 고차함수의 인수로 return을 포함한 람다함수 입력

       return

   }

   println("main end")

}

위의 예제를 보면 inlined 고차 함수에 전달하는 람다 함수의 구현에 return문이 포함되어 있습니다. 이 return은 main함수의 return으로 동작합니다. 따라서 아래쪽에 있는 println("main end")는 실행되지 않습니다.


인라인 고차 함수의 람다 매개변수 접두어

함수 선언 앞에 inline 을 추가하면 인라인 함수를 선언할 수 있습니다.

인라인 고차함수는 말 그대로 고차함수 선언 앞에 inline을 추가한 것입니다. 인라인 고차함수의 람다함수 매개변수에는 noinline, crossinline 접두어를 적용할 수 있습니다.

crossinline

인라인 고차함수에 매개 변수로 입력된 람다함수에 return이 포함되어 있는 경우 인라인 고차함수 내에 선언된 지역 객체(local object)나 중첩 함수(nested function) 영역(실행 문맥)으로는 전달할 수 없습니다.(컴파일 에러 발생) 이런 경우 람다 함수를 입력받는 매개 변수를 crossinline으로 선언하면 return문이 포함된 람다 함수의 입력을 허용하지 않는다는 것을 명시적으로 선언할 수 있습니다.

crossinline 은 noinline이 아닙니다. inline 처리는 하지만 non-local returns 처리는 하지 않는 다는 것을 의미합니다.

noinline

인라인 고차 함수의 매개변수 중 inline처리를 하지 않으려는 매개 변수는 noinline으로 선언 할 수 있습니다. 이 경우 람다 함수에 return을 사용할 수 없습니다.


인라인 고차함수에서 람다 처리 케이스별 분석

원칙적으로 매개변수로 입력된 람다는 인라인 고차함수내에서 인라인화 가능한 방법으로 처리해야 합니다.(실행 문맥이 다른 곳에 람다를 사용하면 안됩니다.) 인라인화가 불가능하도록 처리하는 경우 컴파일 에러가 발생하며 이를 피하려면 매개변수에 적절한 접두어를 추가해야 합니다. 이와 관련된 모든 경우를 예제를 통해 확인해 볼 수 있습니다.(람다함수 매개변수가 noinline으로 선언된 경우는 문제가 발생하지 않아 예제에서 제외시켰다.)


예제

// 인라인 고차함수에서 람다함수를 호출하는 경우

inline fun <T, R> T.callLambda(block: (T) -> R): R {

   return block(this)

}


// 람다함수를 다른 인라인 고차함수에 인자로 전달하는 경우

inline fun <T, R> T.callLambda_delegate(block: (T) -> R): R {

   return this.callLambda(block)

//     return this.useLocalObject(block)

}



// 로컬 변수에 람다함수를 저장 하는 경우

// inline fun <T, R> T.saveToVar ( block: (T)->R ): R {

//     var lambda = block // error: inline parameter는 변수에 저장할 수 없음

//     return block(this)

// }



// 로컬 객체 구현에 람다함수를 사용하는 경우

inline fun <T, R> T.useLocalObject(block: (T) -> R): R {

   var obj = object {

       // 멤버 변수에 람다함수를 저장하는 경우

       //var useInLocalObjectVar = block // error: inline parameter는 변수에 저장할 수 없음


       // 멤버 함수 구현에서 호출하는 경우

       //fun useInLocalObjectFuncCall (): R {

       //    return block(this@useLocalObject) // crossinline으로 선언하면 정상 동작함

       //}


       // 단일 표현식 멤버 함수 구현에 대입 하는 경우

       //fun useInLocalObjectFuncSet (): R = block(this@useLocalObject)

   }


   return block(this)

}


//중첩 함수 구현에 람다함수를 사용하는 경우

inline fun <T, R> T.useInLambda(block: (T) -> R): R {

   var useInLambda = {

       //fun useInLambda (): R = block() // compile error: Kotlin에서 아직 local function을 지원하지 않음


       //block(this@useInLambda) // crossinline으로 선언하면 정상 동작함

   }

   return block(this)//useInLambda()

}


// 인라인 고차함수에서 입력된 람다함수를 인라인이 아닌 고차함수 인자로 전달하는 경우

// inline fun <T, R> T.useLambdaToFuncArg ( block: (T)->R ): R {

//    return useLambdaToFuncArg(this, block) // error: inline parameter는 매개 변수에 저장할 수 없음 noinline으로 선언해야 함

// }

fun <T, R> useLambdaToFuncArg(context: T, block: (T) -> R): R { // 인라인이 아닌 고차함수. 바로 위의 함수에서 호출하는 함수임

   return block(context)

}



//fun main(args: Array<String>) {

fun test() {

   println("main start")


   println("main end")

}


아래는 위의 예제를 편집(주석 제거, 함수 호출 등)하여 각 경우의 실행 결과를 정리한 내용입니다.


람다 함수를 호출하는 경우

  • 함수: callLambda

  • 결과: 정상 동작

  • 인라인화: 가능


로컬 변수에 람다 함수를 저장 하는 경우

  • 함수: saveToVar

  • 결과: inline parameter는 변수에 저장할 수 없음

  • 인라인화: 불가능


로컬 객체 구현에 람다 함수를 사용하는 경우

멤버 변수에 람다 함수를 저장하는 경우

  • 함수: useInLocalObjectVar

  • 결과: inline parameter는 변수에 저장할 수 없음

  • 인라인화: 불가능


멤버 함수 구현에서 호출하는 경우

  • 함수: useInLocalObjectFuncCall

  • 결과: crossinline으로 선언하면 정상 동작함

  • 인라인화: 다른 문맥에 가능 / 람다에 return 사용 불가능


단일 표현식 멤버 함수 구현에 대입 하는 경우

  • 함수: useInLocalObjectFuncSet

  • 결과: crossinline으로 선언하면 정상 동작함

  • 인라인화: 다른 문맥에 가능 / 람다에 return 사용 불가능


중첩 함수 구현에 람다 함수를 사용하는 경우

  • 설명: 인라인 고차함수 내에 구현된 람다 함수 내부에서 인라인 고차함수에 매개변수로 입력된 람다 함수를 호출하도록 구현한 경우

  • 함수: useInLambda

  • 결과: crossinline으로 선언하면 정상 동작함

  • 인라인화: 다른 문맥에 가능 / 람다에 return 사용 불가능


인라인이 아닌 고차함수의 인자로 전달하는 경우

  • 함수: useLambdaToFuncArg

  • 결과: inline parameter는 변수에 저장할 수 없음

  • 인라인화: 불가능


다른 인라인 고차함수에 인자로 전달하는 경우

  • 함수: callLambda_delegate

  • 결과: 정상 동작

  • 인라인화: 가능



종합 결과

사용 관점

1. 인라인 고차함수에 매개변수로 입력된 람다함수가 인라인 고차함수의 실행 문맥에 인라인화 되는 것이 가능하다면 람다함수에서 return문을 사용하는 것이 가능합니다.


2. 인라인 고차함수에 매개변수로 입력된 람다함수가 컴파일 에러 없이 다른 실행 문맥에 인라인화 되기를 원한다면 crossinline으로 선언하여 람다함수에서 return을 사용하지 않는다는 것을 명시해 주어야 합니다.

다른 실행 문맥: object expression 내부에서 사용, 인라인이 아닌 함수의 매개변수로 넘기는 경우 등


1, 2 에 대한 심화: 람다함수를 입력받는 인라인 고차함수(A) 내에서 람다함수를 입력받아 실행하는 인라인 고차함수(B)를 호출하는 경우, B가 A의 실행 문맥에 인라인 처리 되므로 A에서 전달 받은 람다함수를 B에 전달 하여도 결국 A에 B전체가 인라인 처리 됩니다. 따라서 A, B의 inline parameter를 noinline, crossinline등으로 선언하지 않아도 됩니다. (만약 B에서, 입력받은 람다함수를 호출하지 않고 다른 실행 문맥으로 전달하도록 구현된 경우, 즉 B의 inline parameter가 crossinline 등으로 선언되어야 할 경우 B를 호출하는 A의 inline parameter도 crossinline으로 선언되어야 합니다.)


3. 인라인 고차함수에 매개변수로 입력된 람다함수를 다른 변수 또는 다른 함수의 매개변수에 저장( 인라인이 아닌 함수를 호출 하면서 람다함수를 매개변수에 입력 )해야 하는 경우 인라인 처리가 불가능하므로 noinline으로 선언해 주어야 에러가 발생하지 않습니다. 인라인 람다함수는 변수나 매개변수에 저장할 수 없습니다.


제약 관점

1. inline parameter는 매개 변수, 변수 등에 저장할 수 없습니다. 저장 하려면 parameter를 noinline으로 선언해야 합니다.

2. inline parameter(lambda)를 인라인 고차함수가 아닌 다른 실행 문맥에서 인라인 처리 하려면 crossinline으로 선언하여 lambda에서 return을 사용하지 않는다는 것을 보장해 주어야 합니다.

3. inline parameter(람다함수)를 인라인 고차함수(A) 내에서 다른 인라인 고차함수(B)에 넘겨서 처리하는 것은 가능합니다. 단 B의 inline parameter는 A와 선언이 동일해야 합니다. B가 현재 A 안에 인라인 처리 되기 때문입니다. 즉 A와 B는 최종적으로 같은 함수(A를 호출하는 함수)의 구현이 됩니다.


인라인 고차함수에 입력되는 람다에 대한 간단 요약

  1. 람다함수가 인라인 고차함수의 실행 문맥에 인라인화 된다면 return을 사용할 수 있습니다.

  2. 람다함수를 인라인 고차함수 내에서 다른 실행 문맥에 전달하여 인라인 처리를 해야 하는 경우 crossinline으로 선언해야 합니다.

  3. 인라인 처리를 위한 람다함수는 변수, 매개변수 등에 저장할 수 없습니다.

  4. noinline 으로 선언하여 인라인 처리를 포기하면, 거의 대부분의 에러가 해결됩니다.


+ Recent posts