[코틀린] 람다
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) }
}