람다를 사용한 함수형 프로그래밍

·2021년 12월 15일
0
post-thumbnail

📌 함수형 스타일


함수형 스타일이 뭐야?

명령형 프로그래밍에서 우리는 모든 단계를 직접 다뤘어야 했다. i라는 이름의 변수를 만들고, 0으로 초기화하고, i가 k보다 작은지 체크를 한다든지의 행위를 해야했다.

선언적 스타일은 개발자가 원하는 곳을 말하면 호출한 함수가 원하는 것을 처리해준다. 명령어 스타일에서는 세부사항들이 필요하든, 필요없든 항상 눈 앞에 있다. 선언적 스타일에서는 레이어 아래에 캡슐화 되어있다.

함수형 스타일은 선언적 스타일에서 태어났다. 함수형 스타일은 선언적 스타일의 특성과 고차함수를 혼합해놨다.

명령형 프로그래밍

val doubleOfEven = mutableListOf<Int>()
for (i in 1..10) {
    if (i % 2 == 0) {
        doubleOfEven.add(i * 2)
    }
 }

1 부터 10까지의 숫자 중에 짝수의 2배의 값을 계산하기 위해서, 비어있는 뮤터블 리스트를 만들고 콜렉션에서 값이 짝수인지 확인해서 골라낸다. 그리고 뮤터블 리스트에 짝수를 2배로 만들어서 추가한다.

함수형 프로그래밍으 프로그래머들에게 좀 덜 친숙하다. 하지만 매우 간결하다. 우리는 같은 일을 함수형으로 하기 위해서 IntRange 클래스의 filter(), map() 같은 고차함수를 사용할 수 있다.

함수형 프로그래밍

val doubleOfEvenFunc = (1..10)
    .filter { e -> e % 2 == 0 }
    .map { e -> e * 2 }

1~10 까지의 값들에서 짝수만을 필터링 한다. 그리고 선택된 값들을 두 배로 만들어서 맵으로 만든다, 결과는 값들의 이뮤터블 리스트가 된다.

함수형 스타일은 왜, 언제 사용해야 하는가

선언형 스타일의 한 종류인 함수형 스타일을 시작하면 아래와 같은 것들을 깨달을 수 있다.

  • 명령형 스타일은 익숙하지만 복잡하다. 익숙함 때문에 쓰기는 쉽지만 읽기가 매우 어렵다.
  • 함수형 스타일은 좀 덜 친숙하지만 단순하다. 익숙하지 않기 때문에 작성하기는 어렵지만, 읽기가 쉽다.

함수형 스타일은 덜 복잡하다. 하지만 매번 함수형을 쓰는게 명령형을 시용하는 것보다 좋은것은 아니다. 함수형 스타일은 코드가 연산에 집중하고 있을 때 써야한다. 그리고 일련의 변환으로 표현 가능한 문제에 대해서 사용해야 한다.
하지만 많은 입출력이 존재해 뮤테이션이나 부작용을 피할 수 없거나 코드가 많은 수의 예외를 처리해야 한다면 명령형 스타일이 더 좋은 선택이다.

📌 람다 표현식


람다는 고차함수에 아규먼트로 사용되는 짧은 함수이다. 함수에 데이터를 전달하는 대신 우리는 람다를 이용해서 실행 가능한 코드를 함수에 전달할 수 있다.

람다의 구조

람다 표현식은 이름은 없고 타입추론을 이용한 리턴 타입을 가지는 함수이다. 람다는 파라미터 리스트와 바디로 이루어져 있다. 람다는 { }로 감싸져 있고 ->를 이용해서 파라미터와 분리된다.

{parameter list -> body}

람다 전달

람다에 익숙해지기 위해서는 문제가 주어지면 변화하는 단계들을 생각하고, 그 단계들 속에서 우리를 도와줄 함수를 찾아야 한다.

✔ 예를들어, 함수형 스타일로 주어진 숫자가 소수인지 아닌지 구별해주는 함수를 구현해보자

fun isPrime(n: Int) = n > 1 && (2 until n).none({ i: Int -> n % i == 0 })

람다가 어떻게 파라미터로 넘어가는지 살펴보자

코드의 2 until n은 IntRange 클래스의 인스턴스를 리턴한다.
IntRange 클래스는 none() 메소드를 2개의 오버로딩된 버전을 가지고 있다. 그중 하나는 람다를 파라미터로 받는 고차함수이다. 레인지의 값을 람다식에 적용했을 때 그 중 하나라도 람다가 true를 리턴한다면 none() 함수가 false를 리턴한다.

none() 함수는 단축 평가를 한다. 람다가 true를 리턴하는 경우 이후의 요소에 대해서는 평가를 진행하지 않고, none() 함수는 즉시 false를 리턴한다.

none() 함수로 전달된 람다의 파라미터 리스트는 파라미터 i의 타입을 Int로 지정한다. 코틀린은 람다의 파라미터에서 타입을 생략할 수 있다. 람다가 전달된 함수의 파라미터를 기반으로 타입을 추론할 수 있다.

fun isPrime(n: Int) = n > 1 && (2 until n).none { i -> n % i == 0 }

코드가 덜 복잡해졌다. 우리는 여기서 더 간단하게 만들 수 있다.

암시적 파라미터 사용

람다가 하나의 파라미터만 받는다면 파라미터의 정의를 생략하고 it이라는 이름의 특별한 암시적 파람터를 사용할 수 있다.

fun isPrime(n: Int) = n > 1 && (2 until n).none { n % it == 0 }

파라미터를 하나만 취하는 짧은 람다를 쓸 때는 파라미터 정의와 화살표(->)를 생략하고 변수 이름을 it을 사용하도록 하자.

람다 받기

람다를 파라미터로 받는 람다를 만들어보자

fun walk1To(action: (Int) -> Unit, n: Int) = (1..n).forEach { action(it) }

코틀린에서 우리는 n:Int라고 정의하는 것처럼 변수의 이름과 타입을 지정한다. 이런 형식은 람다 파라미터에서도 역시 적용된다.
첫 번째 파라미터의 이름은 action이다. Int를 파라미터로 받고, 아무것도 리턴하지 않는다. 변화된 문법 (type list)-> output type이 사용되었다. 화살표 -> 의 왼쪽은 함수를 받을 인풋 타입을 나타내고, 화살표 ->의 오른쪽은 리턴 타입을 나타낸다.

람다표현식과 Int, 두개의 아규먼트를 전달해서 호출한다.

walk1To({ i -> print(i) }, 5)

코드는 잘 작동하지만 약간 읽기가 어려워 보인다. 파라미터를 재정리하면 함수를 향상시킬수 있다.

람다를 마지막 파라미터로 사용하기

코틀린에서는 람다가 마지막 아규먼트인 경우에 규칙을 조금 풀어주기로 했다. 매우 좋은 기능으로 코드를 간단하게 만들 수 있다. 함수의 파라미터로 람다가 하나 이상 들어간다면 람다 파라미터를 맨 마지막에 사용하자

fun walkTo(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) }

walkTo 호출

✔ 람다를 두 번째 자리에 넣고 호출했다

fun walkTo(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) }

✔ 콤마를 제거하고 람다를 괄호 밖에 놓는것도 가능하다

walk2To(5) { i ->
    print(i)
}

✔ i 대신 암시적 변수 it을 사용할 수도 있다.

walk2To(5) { print(it) }

함수 참조 사용

람다가 단순히 파라미터를 다른 함수로 전달하는 패스스루 람다라면 패스스루 람다는 파라미터가 전달될 함수의 이름으로 대체할 수 있다.

({x -> someMethod(x)})

위 코드를 아래처럼 변경할 수 있다.

(::someMethod)

파라미터를 action으로 보내기만 하는 중개인을 제거했다. 아무런 가치도 더하지 않는 람다는 필요없다.

fun walkTo(n: Int, action: (Int) -> Unit) = (1..n).forEach(action)

walkTo() 함수를 호줄할 때 람다를 제거할 수 있다.

walk3To(5, ::print)

만약 로컬 함수를 참조하고 싶으면 람다를 this의 함수 레퍼런스로 대체 할 수 있다.

fun send(n: Int) = println(n)
walkTo(5) { i -> send(i) }
walkTo(5, this::send)

이와 같은 구조는 싱글톤에서 함수를 사용할 때 사용된다


object Terminal {
    fun write(value: Int) = println(value)
}

walkTo(5) { i -> Terminal.write(i) }
walkTo(5, Terminal::write)

함수를 리턴하는 함수

길이가 n인 이름을 찾는 함수

val names = listOf("Pam", "Pat", "Paul", "Paula")
println(names.find { name -> name.length == 5 }) //Paula
println(names.find { name -> name.length == 4 }) //Paul

예제에 있는 두 람다는 이름의 길이를 제외하면 모두 같은 일을 한다. 코드를 리팩토링해 길이를 파라미터로 받고, 람다를 리턴하는 함수를 만들면 재사용이 가능하다.

fun predicateOfLength(length: Int): (String) -> Boolean {
    return { input: String -> input.length == length }
}

println(names.find(predicateOfLength(5))) //Paula
println(names.find(predicateOfLength(4))) //Paul

predicateOfLength() 함수의 파라미터는 Int타입이고 리턴타입은 함수가 리턴하게 될 함수의 시그니처이다.
String을 파라미터로 받고, Boolean을 출력으로 가진다. 함수가 리턴하는 람다는 input이라는 String 타입의 파라미터를 받고, input의 길이를 predicateOfLength() 함수에 전달된 length 파라미터의 값과 비교한다.

predicateOfLength()가 리턴하는 함수를 보면 함수가 짧고, 블록바디가 없다. 즉, 타입추론이 가능하다.

fun predicateOfLength(length: Int) = { input: String -> input.length == length }

타입추론은 리턴할 함수가 블록바디가 아닌 경우만 사용해야한다.

📌 람다와 익명 함수


람다는 종종 함수의 아규먼트로 전달된다. 그런데 한 람다가 여러곳에서 호출이 필요하다면 중복코드를 생성하게 될 수도 있다. 몇 가지 방법으로 이런 상황을 피할 수 있다.

✔ 재사용을 위해서 람다를 변수에 담는다
✔ 람다 대신 익명함수를 사용한다.

람다를 변수에 저장

람다를 변수에 담아 재사용할 때에 주의사항이 있다. 코틀린은 파라미터의 타입을 추론하지만 변수를 람다를 저장하기 위해 사용한다면 코틀린은 타입에 대한 정보를 알 수 없으므로 타입 정보를 제공해줘야 한다.

val names = listOf("Pam", "Pat", "Paul", "Paula")
val checkLength5 = { name: String -> name.length == 5 }
println(names.find(checkLength5))

변수 checkLength5는 String을 파라미터로 받는 람다를 참조하고 있다. 이 람다의 리턴타입은 코틀린이 추론한다. 왜냐면 타입을 추론하기에 충분한 정보를 가지고 있기 때문이다.
코드를 보면 람다의 파라미터를 지정했고 코틀린은 변수의 타입을 (String) -> Boolean이라고 추정했다.

코틀린에게 반대 방향으로 타입추론을 요청 할 수도 있다. 변수의 타입을 지정해놓고 람다의 파라미터 타입을 추론하게 한다.

val checkLength5: (String) -> Boolean = { name -> name.length == 5 }

이런 케이스에서는 리턴 타입이 지정해 놓은 타입과 다르다면 컴파일 에러가 발생한다.

변수와 람다 모두 타입을 지정해 놓는건 좋은 방법이 아니다.

val checkLength5: (String) -> Boolean = { name: String -> name.length == 5 }

람다가 할당될 변수의 타입을 정의한다면 반드시 리턴타입을 지정해야 한다. 람다의 파라미터 타입을 지정한다면 리턴타입은 타입 추론이 된다.

람다 대신 익명 함수를 사용

익명 함수는 일반 함수처럼 작성한다. 그래서 리턴타입을 지정하는 규칙이 적용된다.
차이점은 단 하나뿐이다. 익명 함수에는 이름이 없다.

val checkLength5 = fun(name: String): Boolean { return name.length == 5 }

익명 함수를 변수에 저장하지 않고, 함수를 호출할 때 직접 아규먼트에 작성해서 사용할 수도 있다.

names.find(fun(name: String): Boolean { return name.length == 5 })

람다를 전달하는 것보다 더 복잡해 보인다. 그러므로 특정 소수의 예외적인 상황을 제외하면 람다 대신 익병함수를 함수 호출에 사용할 이유가 없다.

람다 대신 익명 함수를 사용하는 데에는 약간의 제약사항이 있다. 블록바디 익명함수에는 값을 리턴하기 위해서 return 키워드가 필요하다. return은 감싸고 있는 함수가 아닌 익명 함수에서만 리턴된다. 또한 람다 파라미터가 마지막 파라미터라면 람다를 괄호 밖에서 전달할 수 있지만 익명 함수는 괄호 안에서 사용해야 한다.

names.find{fun(name: String): Boolean { return name.length == 5 }} //ERROR 

📌 클로저와 렉시컬 스코핑


함수형 프로그래밍 개발자들은 람다와 클로저에 대해 이야기 한다. 많은 개발자가 두 개념을 상호교환해 가며 사용하지만 차이점에 대해 잘알고 문맥상 어떤것이 더 적합한지 알아야한다.

람다에는 상태가 없다. 람다의 결과는 입력 파라미터의 값에 달려있다.

람다

val doubleIt = { e: Int -> e * 2 }

람다의 결과는 인풋 파라미터의 두 배이다.

가끔 우리는 외부 상태에 의존하고 싶어한다. 람다는 클로저라고 불린다. 왜냐면 람다는 스코프를 로컬이 아닌 속성과 메소드로 확장할 수 있기 때문이다.

클로저

val factor = 2
val doubleIt2 = { e: Int -> e * factor }

e는 여전히 파라미터이다. 하지만 바디 안에 변수 속성인 factor가 없다. 컴파일러는 변수에 대한 클로저의 범위 즉, 클로저의 바디가 정의된 곳을 살펴봐야 한다. 만약에 클로저가 정의된 곳에서 factor 변수를 찾지 못했다면 클로저가 정의된 곳이 정의된 곳으로 스코프를 확장하고, 또 못 찾는다면 계속 범위를 확장한다.이게 바로 렉시컬 스코핑이다.

위 예제에서는 컴파일러는 변수 factor를 클로저가 정의된 곳 바로 위에 있는 바디에서 찾았다. val factor = 2 부분이다.

뮤터블리티는 함수형 프로그래밍의 금기사항이다. 하지만 코틀린은 클로저 안에서 뮤터블 변수의 값을 잃거나 변경하는 것을 불평하지 않는다. factor는 val로 정의되었지만 var로 변경하면 클로저 안에서 factor을 변경할 수 있다. 하지만 결과를 예상할 수 없어서 혼란을 줄 수 있다.

📌 비지역성(non-local)과 라벨(labeled)리턴


람다는 리턴값이 있더라도 return 키워드를 가질 수 없다. 람다와 익명함수 사이에는 이런 중대한 차이점이 있다. 익명함수는 리턴할 값이 있는경우 반드시 return을 사용해야 한다.

리턴은 허용되지 않는 게 기본이다.

람다에서는 return을 사용하지 않는게 기본이지만 특별한 상황에서 사용할 수 있다.

fun invokeWith(n: Int, action: (Int) -> Unit) {
    println("enter invokeWith $n")
    action(n)
    println("exit invokeWith $n")
}

invokeWith() 메소드는 int와 람다, 2개의 파라미터를 받고 Unit을 리턴한다.

fun caller() {
    (1..3).forEach { i ->
        invokeWith(i) {
            println("enter for $it")
            if (it == 2) {
                //  return Error, return is not allowed here
            }

            println("exit for $it")
        }
    }
    println("end of caller")
}

caller() 함수는 값을 1~3까지 반복하면서 각 값을 이용해서 invokeWith()함수를 호출한다.
암시적 참조인 it을 이용해서 람다의 파라미터다 2일 경우 return키워드를 이용해서 즉시 리턴한다. return을 실행 하자마자 컴파일 실패를 일으킨다. 그 이유는 return의 의미가 명확하지 않기 때문이다.

  1. 즉시 람다에서 빠져나오고 invokeWith()함수의 action(n) 이후의 나머지를 실행하기
  2. for 루프에서 빠져나오기
  3. caller() 함수에서 빠져나오기

코틀린은 이러한 혼란에 빠지지 않기 위해서 return을 허용하지 않는다. 하지만 코틀린은 이 규칙의 2가지 예외를 만들어놨다.

✔ 라벨(labeled, 명시적) 리턴
✔ 논로컬(non-local, 비지역적) 리턴

라벨 리턴

현재 람다에서 즉시 나가야하는 경우에 사용한다.
라벨 리턴은 return@label 형태로 사용하고, label 자리에는 label@ 문법을 이용해서 만든 라멜을 넣을 수 있다.

@here

fun invokeWith(n: Int, action: (Int) -> Unit) {
    println("enter invokeWith $n")
    action(n)
    println("exit invokeWith $n")
}

fun caller() {
    (1..3).forEach { i ->
        invokeWith(i) here@{
            println("enter for $it")
            if (it == 2) {
                return@here
            }

            println("exit for $it")
        }
    }
    println("end of caller")
}

caller()

println("after return from caller")

라벨 리턴은 람다의 흐름을 제어해서 라벨 블록으로 점프하기 위해서 만들어졌다. 다시 말하면 람다를 빠져나가는 것이다. 위의 예제에서, 라벨 리턴은 람다의 마지막 부분으로 뛰어넘는다.

💻 출력

enter invokeWith 1
enter for 1
exit for 1
exit invokeWith 1
enter invokeWith 2
enter for 2
exit invokeWith 2 //람다를 빠져나온다. 
enter invokeWith 3
enter for 3
exit for 3
exit invokeWith 3
end of caller
after return from caller

@here 같이 명시된 라벨을 사용하는 대신 람다가 전달된 함수의 이름같은 암시적인 라벨을 사용할 수도 있다. return@here을 return@invokeWith로 변경하고 here@ 라벨을 제거할 수 있다.

@invokeWith


fun caller() {
    (1..3).forEach { i ->
        invokeWith(i) {
            println("enter for $it")
            if (it == 2) {
                return@invokeWith
            }

            println("exit for $it")
        }
    }
    println("end of caller")
}

논로컬 리턴

람다에서는 기본적으로 return 키워드를 사용할 수 없다. 라벨 리턴을 사용하면 현재의 흐름을 제어하면서 현재 동작중인 람다의 밖으로 나갈수 있다. 논로컬 리턴은 람다와 함께 구현된 현재 함수에서 나갈 때 유용하다.

non-local


fun caller() {
    (1..3).forEach { i ->
        println("in forEach for $i")
        if (i == 2) {
            return
        }
        invokeWith(i) {
            println("enter for $it")
            if (i == 2) {
                return@invokeWith
            }
            println("exit for $it")
        }
    }
    println("end of caller")
}

💻 출력

after return from caller
in forEach for 1
enter invokeWith 1
enter for 1
exit for 1
exit invokeWith 1
in forEach for 2
after return from callerNonLocal

5번째 줄의 리턴은 현재 실행중인 함수를 빠져나간다. 예제에서는 caller()를 빠져나가기 때문에 리턴을 논로컬이라 부른다.

invokeWith() 람다에서는 라벨이 없는 경우 return을 허용하지 않지만 forEach()에 전달한 람다에는 return을 사용할 수 있을까? 💡답은 inline 키워드에 있다.

//invokeWith
fun invokeWith(n: Int, action: (Int) -> Unit) {

//forEach
inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {

🔎 return 키워드

  • return은 람다에서 기본적으로 허용이 안된다.
  • 라벨 리턴을 사용하면 현재 동작중인 람다를 스킵할 수 있다.
  • 논로컬 리턴을 사용하면 현재 동작중인 람다를 선언한 곳 바깥으로 나갈수 있다. 하지만 람다를 받는 함수가 inline으로 선언된 경우에만 사용 가능하다.

🔎 return의 복잡성

  • 람다를 빠져나가기 위해서 언제든지 라벨 리턴을 사용할 수 있다.
  • return을 사용할 수 있다면 람다에서 빠져나가거나 람다를 호출한 곳을 빠져나가는 것이 아니라 람다가 정의된 곳을 빠져나간다는 사실을 기억해야한다.

📌 람다를 이용한 인라인 함수


코틀린은 람다를 사용할 때 호출 오버헤드를 제거하고 성능을 향상시키기 위해서 inine 키워드를 제공한다. inlen 람다는 forEach() 같은 함수에서 리턴을 사용하는 것처럼 논로컬 흐름을 제어를 위해 사용되고 구체화된 타입 파라미터를 전달하기 위해서 사용한다.

인라인 최적화는 없는게 기본

inline 을 배우기 위해 Int와 람다식 두개를 파라미터로 받는 invokeTwo() 함수를 만들자

invokeTwo( )

fun invokeTwo(
    n: Int,
    action1: (Int) -> Unit,
    action2: (Int) -> Unit
): (Int) -> Unit {
    println("enter invokeTwo $n")
    action1(n)
    action2(n)
    println("exit invokeTwo $n")
    return { _: Int -> println("lambda returned from invokeTwo") }
}

함수가 두개의 람다를 실행시키고 함수 안에서 생성된 람다를 리턴한다. 리턴되는 람다는 인풋 파라미터는 무시하고 메세지만 출력한다.

callInvokeTwo( )

fun callInvokeTwo() {
    invokeTwo(1, { i -> report(i) }, { i -> report(i) })
}

callInvokeTwo() 함수를 통해 invokeTwo() 함수를 호출한다. 1을 첫 번째 아규먼트로 전달하고. 두 번째와 세번째 아규먼트로는 report라는 이름의 함수를 호출하는 동일한 람다를 전달했다.

report( )

fun report(n: Int) {
    println("")
    print("called with $n")
    val stackTrace = RuntimeException().stackTrace
    println("Stack depth: ${stackTrace.size}")
    println("Partial listing of the stack:")
    stackTrace.take(3).forEach(::println)
}

report()함수는 현재 실행되고 있는 report() 함수의 콜스택 레벨을 알려준다.

💻 출력

enter invokeTwo 1

called with 1Stack depth: 32
Partial listing of the stack:
chapter10.lambdas.Noinline.report(noinline.kts:25)
chapter10.lambdas.Noinline$callInvokeTwo$1.invoke(noinline.kts:18)
chapter10.lambdas.Noinline$callInvokeTwo$1.invoke(noinline.kts:18)

called with 1Stack depth: 32
Partial listing of the stack:
chapter10.lambdas.Noinline.report(noinline.kts:25)
chapter10.lambdas.Noinline$callInvokeTwo$2.invoke(noinline.kts:18)
chapter10.lambdas.Noinline$callInvokeTwo$2.invoke(noinline.kts:18)
exit invokeTwo 1

인라인 최적화

inline 키워드를 이용해서 람다를 받는 함수의 성능을 향상시킬 수 있다. 함수가 inline으로 선언되어 있으면 함수를 호출하는 대신 함수의 바이트 코드가 함수를 호출하는 위치에 들어가게 된다.
함수 호출의 오버헤드를 제거하지만 함수가 호출되는 모든 부분에 바이트코드가 위치하기 때문에 긴 함수를 인라인으로 사용하는건 좋은 생각이 아니다.
코틀린은 인라인을 사용했을 때 이득이 없는 경우라면 경고해준다.

invokeTwo( ) 최적화

inline fun invokeTwo(
    n: Int,
    action1: (Int) -> Unit,
    action2: (Int) -> Unit
): (Int) -> Unit {
    println("enter invokeTwo $n")
    action1(n)
    action2(n)
    println("exit invokeTwo $n")
    return { _: Int -> println("lambda returned from invokeTwo") }
}

함수의 바디에는 변호가 없다. inline 어노테이션이 함수 정의 앞에 붙었다.

💻 출력

enter invokeTwo 1

called with 1Stack depth: 29
Partial listing of the stack:
chapter10.lambdas.Noinline.report(noinline.kts:25)
chapter10.lambdas.Noinline.callInvokeTwo(noinline.kts:18)
chapter10.lambdas.Noinline.<init>(noinline.kts:20)

called with 1Stack depth: 29
Partial listing of the stack:
chapter10.lambdas.Noinline.report(noinline.kts:25)
chapter10.lambdas.Noinline.callInvokeTwo(noinline.kts:18)
chapter10.lambdas.Noinline.<init>(noinline.kts:20)
exit invokeTwo 1

inline 어노테이션을 추가하기 전의 콜스택의 상위 3개가 사라졌다. 컴파일러는 callInvokeTwo() 함수에서 invokeTwo() 함수용 바이트 코드를 확장했다. 이런 최적화가 report() 호출시 발생하는 오버헤드를 없앨 때까지 계속된다.

선택적 노인라인 파라미터

람다를 최적화 하고싶지 않다면, 람다의 파라미터를 noinline으로 표시하여 최적화를 제거할 수 있다. noinline 키워드는 함수가 inline인 경우에만 파라미터에 사용할 수 있다.

noinline

inline fun invokeTwo(
    n: Int,
    action1: (Int) -> Unit,
    noinline action2: (Int) -> Unit
): (Int) -> Unit {
    println("enter invokeTwo $n")
    action1(n)
    action2(n)
    println("exit invokeTwo $n")
    return { _: Int -> println("lambda returned from invokeTwo") }
}

💻 출력

enter invokeTwo 1

called with 1Stack depth: 29
Partial listing of the stack:
chapter10.lambdas.Noinline.report(noinline.kts:25)
chapter10.lambdas.Noinline.callInvokeTwo(noinline.kts:18)
chapter10.lambdas.Noinline.<init>(noinline.kts:20)

called with 1Stack depth: 31
Partial listing of the stack:
chapter10.lambdas.Noinline.report(noinline.kts:25)
chapter10.lambdas.Noinline$callInvokeTwo$2.invoke(noinline.kts:18)
chapter10.lambdas.Noinline$callInvokeTwo$2.invoke(noinline.kts:18)
exit invokeTwo 1

action1 은 인라인이 되지만 action2 는 noinline을 표시했기 때문에 최적화에서 제외된다.

인라인 람다에서는 논로컬 리턴이 가능하다.

inline fun invokeTwo(
    n: Int,
    action1: (Int) -> Unit,
    noinline action2: (Int) -> Unit
): (Int) -> Unit {
    println("enter invokeTwo $n")
    action1(n)
    action2(n)
    println("exit invokeTwo $n")
    return { _: Int -> println("lambda returned from invokeTwo") }
}

위 코드를 보면 action1 은 인라인이 되었지만 action2()는 noinline으로 표시되었다. 그래서 코틀린은 action1에 전달된 람다에는 논로컬 리턴과 라벨 리턴을 허용하지만,action2에 전달된 람다에는 라벨 리턴만 허용한다. 왜냐하면 인라인 람다는 함수 내에서 확장되지만, 인라인이 아닌 람다는 다른 함수 호출을 사용하기 때문이다.

fun callInvokeTwo2() {
    invokeNoInline(1, { i ->
        if (i == 1) {
            return
        }
        report(i)
    }, { i ->
       // if (i == 2) {return} ERROR 
        report(i)
    })
}

첫 번째 람다가 invokeTwo() 에 전달될 때 논로컬 리턴이 가능하다. 두 번째 람다에서는 코틀린 컴파일러는 논로컬 리턴을 사용할 권한을 주지 않는다.

크로그인라인 파라미터

람다가 호출될지 아닐지 모를 때 인라인으로 만들고 싶다면 어떨까? 호출한 쪽으로 인라인을 번달하도록 함수에게 요청할 수 있다. 이게 바로 크로스라인이다.

ERROR

inline fun invokeTwo(
    n: Int,
    action1: (Int) -> Unit,
    action2: (Int) -> Unit
): (Int) -> Unit {
    println("enter invokeTwo $n")
    action1(n)
    println("exit invokeTwo $n")
    return { input: Int-> action2(input) }
}

invokeTwo가 인라인일 때 내부의 호출인 action1(n) 역시 인라인이 될 수 있다. 하지만 invokeTwo()가 action1를 직접 호출하지 않기 때문에 마지막 줄의 람다에 포함된 action2(input)은 인라인이 될 수 없다.두 번째 파라미터에noinlne 어노테이션이 없기 때문에 충돌이 발생하고 컴파일러 에러가 발생한다.

컴파일 에러를 해결하는 두 가지 방법이 있다

  1. 두 번째 파라미터를 noinline으로 마크한다.
  2. 두 번째 파라미터를 crossline으로 만든다. 이 경우, action2 함수는 invokeTwo() 함수가 아니고 호출되는 부분에서 인라인이 된다.

crossline

inline fun invokeTwo(
    n: Int,
    action1: (Int) -> Unit,
    crossline action2: (Int) -> Unit
): (Int) -> Unit {
    println("enter invokeTwo $n")
    action1(n)
    println("exit invokeTwo $n")
    return { input: Int-> action2(input) }
}
  • inline은 함수를 인라인으로 만들어서 함수 호출의 오버헤드를 제거해서 함수 성능을 최적화한다.
  • crossline도 인라인 최적화를 해주지만 람다가 전달되는 곳이 아니라 실제로 사용되는 곳에서 인라인 최적화가 진행된다.
  • 파라미터로 전달된 람다가 noinlne이나 crossline이 아닌 경우만 논리컬 리턴으로 쓸 수 있다.




🔑 정리


함수형 프로그래밍은 고차함수를 사용하고 유동적인 코드를 쓸 수 있다.

람다는 이름이 없는 함수이다. 그리고 다른 함수의 아규먼트로 쉽게 전달될 수 있는 함수이다.

라벨이 없는 리턴은 항상 함수에서 발생하며 람다에서는 발생하지 않는다.

라벨이 없는 리턴은 인라인이 아닌 람다에서 허용되지 않는다.

일반적으로 코드 최적화를 하기 전에 성능 측정을 먼저 해라. 특히 람다를 사용하는 코드라면 성능 측정을 먼저 해야한다.

inline은 눈에 띄는 성능 향상이 있을 때만 사용해야 된다.



출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글