코틀린

[코틀린] 람다

Parkjuida 2022. 3. 19. 01:20

1. 람다

람다 표현식은 파라미터 리스트와 바디만으로 정의된 함수이다.

// { parameter -> body }
{ e: String -> e + 1 }

람다는 함수형 프로그래밍과 궁합이 잘 맞는다.

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

위의 코드는 소수인지 아닌지를 알려주는 로직을 함수형으로 구현한 것이다. 2 이상이면서 2와 n - 1 까지의 숫자중 어떤 숫자로도 나누어 떨어지지 않으면 소수이다. 2 until n 이 리턴한 IntRange의 none 은 함수를 하나 받아 정수 범위의 원소 하나하나에 함수를 적용시킨다. 모든 결과가 false 라면 none은  true 를 리턴한다.

람다가 한 개의 파라미터를 받는다면 it라는 implicit parameter를 활용해서 람다를 사용할 수도 있다.

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

 

람다를 받는 함수

람다를 받는 함수는 아래와 같이 정의할 수 있다. 아래는 Int -> Int 형의 람다를 받아 내부에서 사용하는 함수이다.

fun modifyOneToTen(func: (Int) -> Int): List<Int> {
    return (1 .. 10).map { func(it) }
}

modifyOneToTen { it * 2 } // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

다른 파라미터와 함께 람다 파라미터를 전달할 때는 마지막 파라미터로 전달하는게 좋다. 예를들어 아래 같이 선언을 하면 좀 긴 람다를 활용해야 할 경우 아래처럼 호출하게된다.

fun manyParameters(func: (Int) -> Int, n: Int): List<Int> {
    return (1 .. n).map { func(it) }
}

manyParameters({ 
    val k = 25
    it * k
}, 5)

코틀린에서 람다를 함수 파라미터의 가장 뒤에 선언하게 되면 아래처럼 함수를 호출하는게 가능하다. 이렇게 호출하는게 위 보다 이해하기가 쉬워 람다를 마지막 함수 파라미터로 선언하는것이 권장된다.

fun manyParameters1(n: Int, func: (Int) -> Int): List<Int> {
    return (1 .. n).map { func(it) }
}

manyParameters1(5){
    val k = 25
    it * k
}

 

:: 사용

함수에 람다를 파라미터로 넘겨주는 방법은 아래와 같았다.

fun doSomething(str: String, func: (String) -> Unit) = func(str)

doSomething("arg") { arg -> println(arg) } // arg 출력
doSomething("it") { println(it) } // it 출력

그런데 이 때 람다는 단지 함수를 호출하기 위한 역할만 하고 있다. 코틀린에서는 이런 경우 좀 더 간편하게 할 수 있는 문법을 제공한다. 함수앞에 ::를 붙이면 람다를 활용할 때와 같은 의미이다.

doSomething("::", ::println) // :: 출력

 

함수를 리턴하는 함수

아래와 같이 람다를 리턴하는 함수를 만들 수도 있다.

fun thisFunctionReturnFunction(n: Int): (Int) -> Boolean {
    return { it -> it % n == 0 }
} // n의 배수이면 true를 리턴하는 람다

println((1 .. 10).filter(thisFunctionReturnFunction(3))) // [3, 6, 9]

 

익명함수

만약 람다를 여러곳에서 재사용하고 싶으면 어떻게할까. 람다는 변수에 할당할 수 있다. 이 때 람다의 파라미터의 타입을 정의해두고 람다의 리턴타입은 추론에 맡기거나, 람다를 할당받는 변수의 타입을 명시할 수 있다.

val tenTimes = { n: Int -> (n * 10).toString()} // 람다 내에 파라미터 타입 지정
tenTimes(10) // 100

val tenTimes: (Int) -> String = { n -> (n * 10).toString() } // 변수에 타입 지정
tenTimes(10) // 100

또 다른 방법으로는 익명함수를 사용하는 것이 있다. 익명함수는 fun, 파라미터 리스트, 바디만으로 이루어진 함수로 변수에 할당할 수 있다.

val tenTimes = fun(n: Int): String { return (n * 10).toString() }
tenTimes(10) // 100

 

2. 클로저(Closure)와 렉시컬 스코핑 (Lexical scoping)

렉시컬 스코핑: 중첩된 함수 선언에서 내부 함수는 외부 함수의 스코프를 포함한다. 외부함수가 내부함수를 리턴하더라도 유지된다.

fun outerFunction(string: String): (String) -> String {
    val name = string
    return { str -> "$name $str"}
}

val function1 = outerFunction("Lexical")
println(function1("Scoping")) // Lexical Scoping

outerFunction이 람다를 리턴할때 람다는 클로저를 형성한다. 클로저는 람다가 형성 될 때의 스코프에 있는 모든 변수들을 담고 있다. 따라서 리턴된 이후에도 람다가 생성될때의 name 값을 그대로 가지고 있다.

 

3. labeled return

기본적으로 람다는 별도의 리턴 값을 가질 수 없다. 아래와 같은 코드는 문법에 맞지 않다.

(1..3).map {
    if(it == 2) return
    it
}
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) {
            println("enter for it")
            if(it == 2) return // 문법적으로 맞지 않음
            println("exit for $it")
        }
    }
    println("end of caller")
}

위의 코드에서도 역시 return은 문법적으로 사용할 수 없다. 코틀린은 이 return이 action(n)을 빠져나오라는 것인지, invokeWith 자체를 빠져나오라는것인지, forEach를 빠져나오라는 것인지 지정해주지 않으면 알지 못한다.

라벨 리턴은 이런 경우에 return이 어디에 적용될지를 정해 준다. 아래와 같이 빠져나올 곳을 label@로 지정해주면 return@label을 만났을때 빠져나올 수 있다.

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()

라벨리턴은 리턴될 곳을 명시할 수도 있고 메소드 이름을 라벨로 사용할 수도 있다.

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")
}

 

4. 인라인 함수

람다를 받는 함수에 inline 키워드를 함수에 붙이면 그 함수가 호출되는 부분에 그 함수의 바이트코드가 들어간다. 이렇게 하면 함수 콜 스택이 줄어드는 장점이 있지만 바이트코드가 커지게 된다. 

아래는 inline 키워드를 붙인 함수와 그렇지 않은 함수의 콜 스택을 출력하는 예제이다.

fun printStackTrace(func: () -> Unit) {
    func()
}

inline fun printStackTraceInline(func: () -> Unit) {
    func()
}

fun call() {
    printStackTrace { println(RuntimeException().stackTrace.size) }
    printStackTraceInline { println(RuntimeException().stackTrace.size) }
}

call()
// 출력
// 34
// 31

inline을 붙인 함수의 호출 스택 수가 더 적다.

만약 일부 람다는 inline 최적화를 하고싶지 않으면 noinline으로 표시하면 된다.

inline fun printStackTraceInline(
    func1: () -> Unit,
    noinline func2: () -> Unit) {
    func1()
    func2()
}

inline 함수는 nonlocal 리턴을 통해 람다를 종료할 수 있다.

inline fun printStackTraceInline(func1: () -> Unit) {
    func1()
}

fun call() {
    printStackTraceInline { (1..5).map {
        if(it == 3) return
        println(it * 10) }
    }
}

// 출력
// 10
// 20

만약 inline 함수의 람다 파라미터로 받은 람다가 함수 내에서 실행되는 것이 아니라 다른 람다로 리턴될경우는 어떨까

이럴 경우에는 noinline으로 다시 인라인 최적화를 풀거나 crossinline으로 호출되는 쪽에서 inline 최적화를 하도록 할 수 있다. crossinline으로 선언된 람다 파라미터의 경우에는 논로컬 리턴은 할 수 없다. 

inline fun printStackTraceInline(crossinline func1: (n: Int) -> Unit): (Int) -> Unit {
    return {input -> func1(input) }
}