Kotlin - 10. Funtional feature

2021/11/03
language

코틀린은 함수형 패러다임을 지원합니다. 따라서 함수를 변수처럼 파라메터나 리턴 타입등으로 활용이 가능합니다.
그리고 Arrow 라는 라이브러리를 사용하면 스칼라 못지 않은 함수형 언어로의 활용도 가능합니다.

람다

1
2
val f1: (Int) -> String = {i -> i.toString()}
val f2 = {i: Int -> i.toString()}

람다는 위와 같이 선언이 가능합니다. f1은 변수타입에 명시적으로 파라메터와 리턴 타입을 표시한 경우이고 f2는 람다 자체에서 명시된 파라메터를 이용하여 해당 람다의 타입을 확인 할 수 있습니다. 물론 두 방식 모두 섞을 수는 있겠지만 코드가 길어지기 때문에 굳이 그렇게 할 필요는 없습니다.

익명함수

람다와 다르게 익명함수 역시 지원합니다. 람다와 다른점은 익명함수는 단순히 함수이기 때문에 리턴등을 명확하게 선언해야 합니다.

1
val f3 = fun(i: Int) {return i.toString()}

역할은 람다와 동일하지만 앞에 fun 지시어와 리턴을 명시적으로 표현한것이 다릅니다. 아무래도 표현식이 더 장황하기 때문에 람다보다는 사용 할 일이 많이 없습니다.

클로저

클로저는 상태가 없는 람다에 상태가 추가된 형태로 볼 수 있다. 외부 변수를 위에 범위가 확장되어있고 이를 위해 람다의 스코프가 확장되는 것을 랙시컬 스코핑(Lexical scoping)이라고 부릅니다.

클로저가 외부 변수를 참조할 땐 해당 변수는 변경불가능한 상태를 사용하는것이 좋습니다. 변화가 가능하면 해당 함수는 참조무결성이 깨지기 때문에 함수형 프로그래밍을 진행하기 어렵습니다.

리턴

labeled 리턴

일반적으로 코틀린의 람다는 리턴 키워드를 사용 할 수 없습니다. 람다에서 리턴 키워드를 사용하면 코틀린에서는 람다를 감싸는 외부 함수의 리턴으로 인식하게 됩니다.

1
2
listOf(1,2,3)
.filter {return (it % 2) == 0} // not lambda return

그래서 코드 블록에 라벨을 추가하여 해당 블록을 빠져나가도록 명시적으로 지정 할 수 있습니다.

1
2
listOf(1,2,3)
.filter odd@{return@odd (it % 2) == 0} // lambda return

혹은 람다를 파라메터로 받는 함수의 이름으로도 대체가능합니다.

1
2
listOf(1,2,3)
.filter {return@filter (it % 2) == 0} // label 'filter' matched with method that receive lambda

논로컬 리턴

아까 위에서 람다에서 그냥 리턴하는 경우엔 외부함수의 리턴이라고 언급했습니다. 하지만 이는 선제조건이 필요합니다. 바로 실행하는 함수가 inline 함수여야 한다는 점입니다.

1
2
3
4
5
fun test(i: (Int) -> Boolean) {
println(if (i(10)) "pass" else "fail")
}

test {return it > 5} // ERROR return is not allow here

위와 같이 정의한 경우 컴파일 에러가 납니다. inline 함수가 아니기 때문입니다. 왜 인라인 함수여야하는지는 아래에서 다루도록 하겠습니다.

1
2
3
4
5
6
7
inline fun test(i: (Int) -> Boolean) {
println(if (i(10)) "pass" else "fail")
}

fun someFunc() {
test {return it > 5} // ERROR inferred type is Boolean but Unit was expected
}

리턴을 하였을 때 외부 함수의 리턴타입과 매치되지 않아 컴파일 에러가 발생합니다. 네 someFunc는 unit 을 리턴하기 때문에 실패하는 것입니다.

1
2
3
4
5
6
7
inline fun test(i: (Int) -> Boolean) {
println(if (i(10)) "pass" else "fail")
}

fun someFunc() {
test { return@test it > 5 }
}

리턴에 별도의 라벨을 추가하여 test 메소드에서 실행될 함수 라벨을 이용하여 람다를 리턴하게 할 수 있습니다.
이렇게 외부 함수가 아닌 실행 중인 람다에서 빠져나오게 하는 리턴을 논로컬 리턴이라고 합니다.

인라인 함수의 최적화

코틀린에서 inline 키워드를 통해 함수가 일반적인 형태로 호출 되면 이 함수는 런타임에 기존의 함수와는 다르게 취급됩니다.
함수 내용자체가 함수 호출부에 반영되어 함수가 호출되는 형태가 아닌 컴파일 타임에 함수 내 코드가 호출부에 복사되는 형태로 바뀌게 됩니다.
앞에서 본 예제에서도 inline 함수에서 사용된 람다 역시 마찬가지로 함수와 마찬가지로 람다 자체도 실행 부분에 복사되게 됩니다.

따라서 함수 호출을 줄임으로서 실행의 최적화가 가능합니다.
대표적인 예로 forEach를 들 수 있는데, inline 으로 선언된 덕분에 자바와는 다르게 추가 오버헤드 없이 람다를 실행하여 성능을 최적화 할 수 있게 되었습니다.

만약 inline 이 아니었다면 어땠을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {

println("normal lambda")
listOf(1).each { showStack() }
println()
println("inline lambda")
listOf(1).forEach { showStack() }
}

fun showStack() {
val arrayOfStackTraceElements = Thread.getAllStackTraces()[Thread.currentThread()]
println(arrayOfStackTraceElements?.map { it.toString() }?.joinToString("\n"))
}

fun <T> List<T>.each(lambda: (T) -> Unit) {
for (i in this) {
lambda(i)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
normal lambda
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getAllStackTraces(Thread.java:1610)
TestKt.showStack(Test.kt:53)
TestKt$main$1.invoke(Test.kt:47)
TestKt$main$1.invoke(Test.kt:47)
TestKt.each(Test.kt:67)
TestKt.main(Test.kt:47)
TestKt.main(Test.kt)

inline lambda
java.lang.Thread.dumpThreads(Native Method)
java.lang.Thread.getAllStackTraces(Thread.java:1610)
TestKt.showStack(Test.kt:53)
TestKt.main(Test.kt:49)
TestKt.main(Test.kt)

showStack() 은 현재스레드의 스택스레이스를 표시하는 함수 입니다. 별도로 정의한 each라는 함수는 inline이 아니기 때문에, 별도의 invoke 절차를 통해 람다가 실행된 것을 알 수 있습니다. 하지만 inline으로 선언된 forEach는 람다의 invoke가 없는 것을 알 수 있습니다.

인라인 람다 해제

다만 람다가 인라인처리가 되면 람다가 어떤 데이터로 취급되는 것이 아니라 실제 코드상에 람다 내용이 덮어씌워지게 됩니다. 따라서 람다를 별도의 변수로 취급하거나 리턴하거나 할 수 없습니다.

1
2
3
4
5
6
7
8
inline fun <T> List<T>.each(lambda: (T) -> Unit): (T) -> Unit {
val l = lambda // Illegal usage of inline-parameter 'lambda'
for (i in this) {
lambda(i)
}

return lambda // Illegal usage of inline-parameter 'lambda'
}

이런 람다의 인라인 처리는 인라인 함수에서는 기본으로 반영되는데, 이를 막고 싶은 경우엔 noinline이라는 지시어를 통해 막을 수 있습니다.

1
2
3
4
5
6
7
inline fun <T> List<T>.each(noinline lambda: (T) -> Unit): (T) -> Unit {
for (i in this) {
lambda(i)
}

return lambda
}

noinline을 통해 다시 람다를 함수 외부로 넘길 수 있게 되었습니다.

크로스인라인

인라인 람다를 다른 곳에 넘기는건 완전 불가능할까요? 이를 위해 코틀린은 crossinline 이라는 것도 만들었습니다.

1
2
3
4
5
6
7
inline fun <T> List<T>.each(crossinline lambda: (T) -> Unit): (T) -> Unit {
for (i in this) {
lambda(i)
}

return { lambda(it) } // crossinline make this possible
}

crossinline은 inline의 특성을 유지하면서 해당 람다를 외부로 옮길 수 있는 inline 타입입니다.
위 예제에서 마지막 리턴문을 보면 람다 내부에 crossinline 으로 선언된 lambda가 실행되는 것을 확인할 수 있습니다. 다만 inline 이기 때문에 변수로 취급하거나 람다 자체를 리턴하는건 불가능합니다.
(물론 실행이 아닌 바이트코드가 해당 부분에 삽입되는 형태입니다.)


출처

다재다능 코틀린 프로그래밍
코틀린 공식문서