익명 함수 (Anonymous Functions, a.k.a. 람다) 는 코틀린에서 중요한 부분을 차지하고 있다. 일단 코틀린 표준 라이브러리에서 제공되는 빌트인 함수들을 쉽게 커스터마이징 할 수 있게 한다.
예시를 살펴보자.
count() 함수의 경우. String에서 사용되면 글자 수(length 프로퍼티)를 반환하게 된다.
만약에 Mississippi에서 s의 수를 세고 싶다면 어떻게 해야 할까? count 함수에 새로운 규칙을 제공해주어야 한다. 함수의 인자로 람다를 넣으면 된다.
한 글자씩 진행하면서 람다가 참이면 카운트를 높이고 모든 글자를 검사하면 count는 최종 카운트 결과를 반환한다.
println()
println({
val year = 2019
val month = 12
val date = 31
"Today : $year/$month/$date" // Today : 2019/12/31
}())
중괄호 안에 람다의 정의를 할 수 있다. 이 람다에서 변수를 정의하고 String을 반환하고 있다. 중괄호를 닫고 나서 일반적인 함수처럼 ()를 해주어야 호출이 된다.
함수 타입의 변수를 만들 수 있다. C 언어가 익숙하다면, 함수 포인터 개념과 비슷하다.
() -> String 이 todayStamp의 타입이 된다.
val todayStamp : () -> String = {
val year = 2019
val month = 12
val date = 31
"Today : $year/$month/$date"
}
println(todayStamp())
필요한 매개변수의 타입을 () 안에 넣어주고 -> 뒤에 리턴값의 타입들을 넣어주면 된다.
왜 return 키워드가 없는데도 String이 리턴되고 있을까? 대부분의 경우에 람다는 return 키워드 없이도 정의 부분의 마지막 줄을 리턴하게 된다. 생략해도 되는 것이 아니라 쓰면 안된다. 익명 함수를 호출한 함수에서 리턴하는 것인지 익명 함수에서 리턴하는 것인지 모호하기 때문이다.
람다도 매개 변수를 가질 수 있다.
val todayStamp2 : (Int, Int, Int) -> String = { y,m,d ->
"Today : $y/$m/$d"
}
println(todayStamp2(2019, 1, 1))
타입 추론이 힘들기 때문에 명시적으로 타입을 선언해 줘야 한다.
람다가 단 하나의 매개변수를 가진다면, 매개변수 이름 대신에 it 키워드를 사용할 수 있다. 매개변수가 하나인 경우에는 it와 매개변수의 이름 중 어떤 것을 사용해도 유효하다.
전의 count 함수 예시에서도 it 키워드를 사용할 수 있다.
함수에서도 타입 추론이 일어난다.
val todayStamp : () -> String = {
val year = 2019
val month = 12
val date = 31
"Today : $year/$month/$date"
}
를
이렇게 타입을 생략해도 된다. 람다가 여러개의 매개변수를 가질 때에는 추론이 어려울 수 있다. 전의 todayStamp2 변수에서 타입을 없애면 컴파일러가 타입 추론을 어려워한다. 그럴 때에는 함수 정의부에서 매개변수의 타입을 명시해주면 된다.
함수 매개변수로 함수를 넣을 수 있다.
fun printRandomDate(year: Int, stamp : (Int, Int, Int) -> String)
{
val month = (1..12).shuffled().last()
val date = when(month)
{
1,3,5,7,8,10,12 ->(1..31).shuffled().last()
2 -> (1..28).shuffled().last() // if no leap year...
else -> (1..30).shuffled().last()
}
println(stamp(year, month, date))
}
fun main()
{
printRandomDate(2019, todayStamp2)
}
함수의 마지막 매개변수가 함수 타입인 경우에 한해서 람다 매개변수 외곽의 소괄호를 제거한다.
여러 매개변수를 사용할 때에는 축약 문법을 사용하려면 함수 타입 매개변수를 마지막 매개변수로 두어야 한다.
위의 printRandomDate 예시에 축약 문법을 적용해보자.
fun printRandomDate(...) // 동일한 문법
fun main(){
printRandomDate(2019) { y : Int, m : Int, d : Int -> "Today : $y/$m/$d" }
}
매우 깔끔하게 코드가 정리되었다.
람다를 사용하는 건 편하고 유연성 있는 코드를 만들어주지만, 좋은 점만 있는 것은 아니다. 람다를 정의하면 JVM에 객체 인스턴스로 나타나고, JVM은 람다에서 접근 가능한 모든 변수에 메모리를 할당해주어야 한다. 결국 람다를 사용하면 성능 문제에 직면하게 된다. 다행히 람다를 사용하면서도 이런 오버헤드를 없애는 최적화 방법이 있다. 이게 바로 inlining이다. Inlining은 JVM이 객체 오브젝트를 사용하고 JVM이 람다에 변수 메모리 할당을 할 필요성을 제거한다.
람다를 inline으로 바꾸려면 람다 함수 앞에 inline 키워드를 사용하면 된다.
inline fun printRandomDate(year: Int, stamp : (Int, Int, Int) -> String)
{
val month = (1..12).shuffled().last()
val date = when(month)
{
1,3,5,7,8,10,12 ->(1..31).shuffled().last()
2 -> (1..28).shuffled().last() // assume that no leap year
else -> (1..30).shuffled().last()
}
println(stamp(year, month, date))
}
이렇게 하면 printRandomDate 함수를 호출하는 대신에 컴파일러에서 함수 본체를 그대로 복사해서 붙여넣게 된다. 바이트코드를 확인해보면 확실히 알 수 있다.
일반적인 상황에서는 람다를 사용하는 함수에서 inline 키워드를 쓰는 것은 좋은 방법이지만, 몇몇 경우에는 불가능하다. 예를 들어 재귀함수인 경우 코드를 그대로 넣으려면 무한 루프에 빠지게 된다. 컴파일러가 inline을 사용하면 안된다고 경고를 보낼 것이다.
지금까지 람다를 정의해서 다른 함수의 매개변수로 전달해왔다. 이걸 다른 방식으로 할 수 있다. 함수 레퍼런스 를 사용하는 방법이다. 함수 레퍼런스는 fun으로 정의된 이름이 있는 함수를 다른 함수에 전달 가능한 매개변수로 변환해준다. 람다식을 사용하는 어떤 곳이든 함수 레퍼런스를 사용할 수 있다.
새로운 todayStamp 함수를 만들고 함수 레퍼런스를 통해서 printRandomDate로 전달하는 예시이다.
fun todayStamp3(y : Int, m : Int, d : Int) : String{
val month = when(m){
1 -> "Jan"
2 -> "Feb"
3 -> "Mar"
4 -> "Apr"
5 -> "May"
6 -> "Jun"
7 -> "Jul"
8 -> "Aug"
9 -> "Sep"
10 -> "Oct"
11 -> "Nov"
12 -> "Dec"
else -> "What else?"
}
return "Today : $month $d $y"
}
fun main(){
printRandomDate(2019, ::todayStamp3)
}
매개변수로도 되니까 당연히 리턴 타입으로도 가능할 거라고 생각할 것이다. 당연히 된다.
함수를 매개변수로 하거나 리턴하는 함수를 고차 함수(higher-order function) 라고 한다.
코틀린에서 익명 함수는 함수 가시거리 밖에 있는 변수들을 수정하고 참조할 수 있다. 이것은 익명 함수가 만들어진 곳에서 정의된 변수들에 대한 레퍼런스를 익명 함수가 가지고 있다는 것을 뜻한다.
fun main() {
printRandomDate()
}
val todayStamp2 = { y: Int, m: Int, d: Int ->
"Today : $y/$m/$d"
}
fun todayStamp3(y: Int, m: Int, d: Int): String {
val month = when (m) {
1 -> "Jan"
2 -> "Feb"
3 -> "Mar"
4 -> "Apr"
5 -> "May"
6 -> "Jun"
7 -> "Jul"
8 -> "Aug"
9 -> "Sep"
10 -> "Oct"
11 -> "Nov"
12 -> "Dec"
else -> "What else?"
}
return "Today : $month $d $y"
}
fun todayStamp4(): (Int, Int, Int) -> String {
return { y: Int, m: Int, d: Int ->
"$m-$d-$y"
}
}
fun chooseStamp(stampNum: Int): (Int, Int, Int) -> String
{
return when (stampNum) {
2 -> todayStamp2
3 -> ::todayStamp3
else -> todayStamp4()
}
}
fun stampGenerator() : (Int) -> String
{
val month = (1..12).shuffled().last()
val date = when (month) {
1, 3, 5, 7, 8, 10, 12 -> (1..31).shuffled().last()
2 -> (1..28).shuffled().last() // assume that no leap year
else -> (1..30).shuffled().last()
}
var count = 0
return {
stampNum : Int ->
count+=1
val stamp = chooseStamp(stampNum)
"Stamp $count\n${stamp(2019, month, date)}"
}
}
fun printRandomDate() {
val stamp = stampGenerator()
println(stamp(2))
println(stamp(3))
println(stamp(4))
}
출력 결과 : Stamp 1 Today : 2019/8/9 Stamp 2 Today : Aug 9 2019 Stamp 3 8-9-2019
stampGenerator 함수에 주목해보자. 이 함수는 (Int) -> String 타입의 람다를 리턴하는 함수이다. 이 리턴된 람다를 가지고 printRandomDate() 에서 출력을 하게 된다.
stampGenerator 함수에 있는 count 변수는 람다 외부에서 선언되었다. 하지만 람다 변수에서 count+=1과 $count를 통해서 참조 가능하고 수정 가능하다.
이렇게 되는 이유는 코틀린의 람다는 클로저(closure) 이기 때문이다. 클로저는 클로저가 정의된 바깥쪽의 변수들을 참조, 수정할 수 있다.
Kotlin Programming: The Big Nerd Ranch Guide (공)저: Josh Skeen, David Greenhalgh