고차함수와 인라인 함수

CmplxN·2020년 11월 15일
0

Kotlin은 함수형 프로그래밍(Functional Programming)을 지원하는 언어다.
물론 Kotlin은 함수형 프로그래밍을 지원할 뿐, 굳이 함수형 프로그래밍을 하지 않아도 된다.
그렇지만 때때로 함수형 프로그래밍을 이용하면 편하게 구현할 수 있다.
또한 코틀린에서 제공되는 여러 함수를 정확히 사용하려면 인라인에 대한 이해가 필요하다.

일급 객체로서의 함수

함수형 프로그래밍의 원칙 중 하나로 "일급 객체로서의 함수"가 있다.

최상위 구성요소

이 말은 첫번째로 함수가 프로그램의 최상위 구성요소라는 것이다.
함수가 최상위 구성요소가 되면 함수내에 함수, 변수 심지어 클래스도 들어갈 수 있다.
Java에서는 모든 것은 클래스안에 위치해야했다.
하지만 Kotlin에서는 함수가 프로그램 최상위 구성요소가 될 수 있으므로 아래와 같은 코드가 가능하다.

fun test() {
    val hello = 1
    fun world() = 2
    class Test
}

고차함수

또한 위에서 언급한 최상위 요소랑 연결되는 사항으로, 함수를 인자로 넘길 수 있고, 함수를 반환할 수 있어야한다.
함수라는걸 뭔가 특별하다고 생각하지 않고, 일반 변수처럼 굴릴 수 있다고 생각하면 이해하기 쉬울 것이다.

그러니까

fun test(num: Int): Double = num.toDouble()

하듯이 저 자리에 함수를 넣어도 된다는 것이다.

예시를 보자.

fun testFun(a: (String) -> Int): (Int) -> Int {
    return { i: Int -> i + a("Hello") }
}

testFun을 보면
(StringInt로 변환하는 함수)를 인자로 받아서 (IntInt로 변환하는 함수)를 반환하고 있다.
이렇게 매개변수로 함수를 받거나 함수를 반환하는 함수를 고차함수(High-Order Function)이라고 한다.

Inline 함수

함수의 fun 앞에 inline을 붙이면 그 함수를 inline 함수로 만들 수 있다.
inline 함수는 inline 함수가 호출되는 곳에 컴파일 단계에 정적으로 포함된다.

대체 저게 무슨말이냐고 할 수 있다.
백문이 불여일견이라고 예제를 통해서 inline 함수의 용도를 살펴보자.

inline을 쓰지 않은 예제

아래는 간단하게 (두 정수를 한 정수로 만드는 함수)를 인자로 받는 함수다.

fun hoFunTest(argFun: (Int, Int) -> Int) {
    argFun(10, 20)
}

fun main() {
    hoFunTest { x1, x2 -> x1 * x2 }
}

대게 Kotlin은 JVM을 쓰므로 위 Kotlin 코드를 Java로 변환해보자.

public static final void hoFunTest(@NotNull Function2 argFun) {
   Intrinsics.checkNotNullParameter(argFun, "argFun");
   argFun.invoke(10, 20);
}
public static final void main() {
   hoFunTest((Function2)null.INSTANCE);
}

Java 코드를 보면 Kotlin 코드를 그대로 옮겨놓은 것을 볼 수 있다.
main()에서 hoFunTest()를 호출하고 hoFunTest()에서 넘겨받은 argFun()을 호출하는 구조다.

inline을 쓴 예제

같은 예제에서 이번에는 hoFunTest()에 inline 키워드를 붙여보자. 즉 이렇게 바꿔보자.

inline fun hoFunTest(argFun: (Int, Int) -> Int) {
    argFun(10, 20)
}

그러면 Java에서는

public static final void main() {
   int $i$f$hoFunTest = false;
   int x2 = 20;
   int x1 = 10;
   int var3 = false;
   int var10000 = x1 * x2;
}

로 변환된다.
아까랑 확연하게 다른 점이 하나 있다.
main()에서 다른 함수를 call 하지 않고 hoFunTest()main()에 포함되어버린 것이다.
즉 아까 위에서 말했듯이 inline 함수가 호출되는 곳main()컴파일 타임에 포함되어버린 것이다.
코드 자체가 복사되었다고 이해하면 된다.

inline의 장점

inline을 쓰면 런타임 최적화를 할 수 있다.
inline을 쓰지 않은 예제의 java코드에서 확인할 수 있듯이,
inline을 사용하지 않으면 같은 행동을 함에도 불구하고, 함수 호출 횟수는 더 많아진다.
그리고 함수 호출시 이 람다함수들은 Function이라는 Java 객체로 변환된다.
결국 이 말은 inline을 쓰지 않아서 쓸데없이 객체가 생성되고 런타임 자원을 소모하게 된다는 것이다.
고차함수에 inline을 걸면 이런 낭비를 줄일 수 있다.

또한 논 로컬 리턴을 사용할 수 있다.
원래 코틀린 람다함수에서는 return을 사용할 수 없다.
하지만 람다함수가 inline함수의 인자라면 return을 사용할 수 있다.
이를 이용해서 람다함수 또는 inline을 호출한 함수에서 return할 수 있다.

noinline과 crossinline

noinline

어떤 함수에 두 함수 A와 B를 인자로 받는다고 하자.
이때 A는 inline을 하고 싶지만, B는 inline으로 하고 싶지 않을 수 있다.
(최종 코드 길이가 길어진다던가, 재귀불가, private 키워드 사용의 제한 등 단점이 없지 않다.)
그런 경우 아래와 같이 함수명에는 inline 키워드를 붙이되, 필요한 인자에 noinline을 붙여 작성하면 된다.

inline fun hoFunTest(A: (Int) -> Int, noinline B: (Int) -> Int)

crossinline

inline 함수의 매개변수인 함수 C가 return을 포함하지 않도록 강제해야하는 경우가 있다.
함수 C가 inline 함수에서 직접 불리는 것이 아니라 다른 실행 컨텍스트를 통해 호출되는 경우가 그런 경우다.
이런 경우 함수 C 앞에 crossinline 키워드를 붙이면 된다.

inline fun hoFunTest(crossinline C: (Int) -> Int)

참고

profile
Android Developer

0개의 댓글