-
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) } }
'코틀린' 카테고리의 다른 글
[코틀린] 연산자 오버로딩 (0) 2022.03.20 [코틀린] 내부 반복(map, filter, groupBy, reduce) (0) 2022.03.19 [코틀린] 델리게이션 (0) 2022.03.16 [코틀린] 클래스와 상속 (0) 2022.03.13 [코틀린] 객체와 클래스 (0) 2022.03.13