본문 바로가기
java & kotlin

[kotlin] 고차함수와 람다

by algosketch 2021. 12. 13.

 ※ 이 글은 고차함수와 람다에 대한 기본 개념을 설명하지는 않습니다.

  • inline 과 람다
  • 고차함수
  • return

 

inline 과 람다

 람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무영 클래스 객체가 생성된다. 이는 람다 사용에 따른 부가 비용이다. inline 함수와 람다를 함께 사용하면 람다로 받은 부분을 컴파일 시점에 대치한다. 따라서 런타임에 더 빠르게 동작한다. 다음 코드를 보자.

fun main(args: Array<String>) {
    doSomething { println("action") }
}

inline fun doSomething(action : () -> Unit) {
    println("before action")
    action()
    println("after action")
}

 람다를 파라미터로 받는 함수는 inline 키워드를 사용할 수 있다. 이렇게 선언된 함수는, 컴파일 시점에 함수를 호출하는 부분을 모두 람다 본문으로 바꿔치기 한다. 따라서 위 코드만 실행한다면 아래 코드와 같이 동작한다.

fun main(args: Array<String>) {
    doSomething()
}

inline fun doSomething(action : () -> Unit) {
    println("before action")
    println("action")
    println("after action")
}

 람다를 사용하기 위해 런타임에 무명 객체를 생성하는 비용을 아낄 수 있다. 다만, 함수를 호출하는 모든 코드를 본문으로 대치하기 때문에 컴파일된 코드의 길이가 매우 길어질 수 있다. 람다의 본문이 길고 함수를 inline 함수를 호출하는 부분이 많다면 inline 함수를 사용하기 부적절한 상황일 수 있다. 
 또한 파라미터로 받은 람다를 바로 사용하는 것이 아니라 변수에 저장한 후 사용하는 경우엔 inline 함수를 사용할 수 없다. 람다를 변수에 저장하는 시점에 무명 객체를 생성해 저장해야하기 때문이다.

 

고차함수

 forEach, filter, map 과 같은 컬렉션함수나 고차 함수는 inline 함수이다. 따라서 이런 함수를 사용한다고 해서 성능 저하가 일어나지 않는다. 실제로 forEach 는 for 문의 동작과 동일하다.
 filter 나 map 같은 고차함수를 연쇄적으로 사용하면 어떻게 될까? 각 함수들은 인라인된다. 이러한 고차함수는 중간 결과 리스트를 저장하기 때문에 리스트의 크기가 클수록 연산이 부담된다. 이럴 경우 asSequence 를 통해 성능을 향상시킬 수 있지만, asSequence 를 사용하는 경우 함수가 인라인되지 않기 때문에 리스트의 크기가 작을 경우에 asSequence 를 사용하면 오히려 성능이 떨어질 수 있다.

 

return

 람다에서 return 하면 어떻게 될까? 결론부터 말하면, return 은 fun 으로 정의된 부분을 반환한다. 람다는 fun 키워드를 사용하지 않는다. 무명 함수나 람다 본문에서 return 을 사용할 경우 무명함수를 넘기는 경우와 람다를 넘기는 경우 동작이 다르다. return 을 사용하는 방법은 하나씩 알아보자.

넌로컬(non-local)

fun main(args: Array<String>) {
    doSomething()
}

fun doSomething() {
    (1..10).forEach {
        println("몇 번 출력될까?")
        return
    }
    println("얘는 출력될까?")
}

 위 코드에서 println 은 몇 번 출력될까? 정답은 한 번이다. 위 코드에서 forEach 는 inline 함수로 실제로는 아래 코드처럼 동작한다.

fun main(args: Array<String>) {
    doSomething()
}

fun doSomething() {
    for ( i in 1..10 ) {
        println("몇 번 출력될까?")
        return
    }
    println("얘는 출력될까?")
}

 인라인 함수의 경우 위 코드처럼 대치되기 때문에 "얘는 출력될까?" 부분은 출려되지 않고 바깥에 있는 함수 자체를 반환해 버린다. 이 경우를 넌로컬이라고 한다.

label

 해당 람다만 빠져나가고 싶을 땐 label 을 사용할 수 있다. label 사용 방법은 명시적으로 레이블을 붙이는 경우와 인라인 함수 이름을 사용하는 방법이 있다.

fun main(args: Array<String>) {
    doSomething()
}

fun doSomething() {
    (1..10).forEach label@{
        if(it == 3) return@label
        println("$it 몇 번 출력될까?")
    }
    println("얘도 출력된다.")
}
fun main(args: Array<String>) {
    doSomething()
}

fun doSomething() {
    (1..10).forEach {
        if(it == 3) return@forEach
        println("$it 몇 번 출력될까?")
    }
    println("얘도 출력된다.")
}

 kotlin in action 책에 의하면 for 루프의 break 문과 비슷하다고 설명되어 있어 헷갈릴 수 있는데, 위 코드에선 forEach 에 대한 continue 라고 표현해야 더 정확한 것 같다. 위 코드들에서는 총 9번 출력 된다.

무명 함수

fun main(args: Array<String>) {
    doSomething()
}

fun doSomething() {
    (1..10).forEach (
        fun (it: Int) {
            if(it == 3) return
            println("$it 몇 번 출력될까?")
        }
    )
    println("얘는 출력될까?")
}

 위 코드는 이전 코드와 동일하게 동작한다. 기본적으로 return 은 fun 으로 깜싼 부분을 반환하기 때문에 무명 함수의 경우 label 을 통해 반환하지 않아도 된다.