고차함수와 람다
고차함수
고차함수는 함수를 파라매터로 받고, 함수를 반환하는 함수. 좋은 예가 lock()
이다
fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}
body
는 () -> T
라는 함수타입을 가진다. body는 파라매터를 가지지 않고, T
타입 값을 반환하는 함수이다. try 블록안에서 호출되고 lock
에 의해 보호되고 그 결과는 lock()
함수의 결과로 리턴한다.
lock()
함수를 호출하고 싶으면, 인자로 다른 함수를 전달할 수 있다 (함수 참조 참고)
fun toBeSynchronized() = sharedResource.operation()
val result = lock(lock, ::toBeSynchronized)
다른 간편한 방법은 람다식을 전달하는 것이다
val result = lock(lock, { sharedResource.operation() })
람다식에 대해서는 뒤에 자세하게 설명하지만, 간단한 개념을 보자
- 람다 표현식은 항상 중괄호에 둘러싸여 있다
->
전에 파라매터(있으면) 선언한다 (파라매터 타입은 생략 가능)->
뒤에 몸체가 온다 (있을 경우)
Kotlin에서 함수의 마지막 파라매터가 함수이고, 람다식을 인자로 전달하는 경우, 괄호 밖에서 지정가능
lock (lock) {
sharedResource.operation()
}
고차함수의 다른 예는 map()
이다
fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
val result = arrayListOf<R>()
for (item in this)
result.add(transform(item))
return result
}
이 함수는 다음과 같이 호출 가능
val doubled = ints.map { it -> it * 2 }
함수를 호출할때 람다가 유일한 인수라면 괄호는 완전히 생략 가능
it: 단일 파라매터의 암묵적인 이름
다른 유용한 규칙으로, 함수 리터럴의 파라매터가 하나인 경우 해당 선언을 생략 가능 (->
포함) 하며, 파라매터 이름은 it
이 된다
ints.map { it * 2 }
이런 규칙에 따라 LINQ 스타일로 코드를 작성 가
strings.filter { it.length == 5 }.sortBy { it }.map { it.toUpperCase() }
미사용 변수에 밑줄 (1.1 이후)
람다 파라매터가 미사용인 경우, 이름 대신에 밑줄을 사용 가능
map.forEach { _, value -> println("$value!") }
람다 파괴 (1.1 이후)
람다에서의 파괴는 파괴 선언의 일부로 기술된다
인라인 함수
인라인 함수를 사용하여 고차 함수의 성능을 향상 시킬 수 있다.
람다 식과 익명 함수
람다 식 혹은 익명함수는 "함수 리터럴"로 선언없이 식으로 전달되는 함수이다.
max(strings, { a, b -> a.length < b.length })
함수 max
는 고차 함수이며, 즉 함수 값을 두 번째 인수로 취합니다. 이 두 번째 인수는 그 자체로 함수, 즉 함수 리터럴 인 표현식입니다. 함수로서 이 식은 아래와 같다.
fun compare(a: String, b: String): Boolean = a.length < b.length
함수 타입
함수가 다른 함수를 파라매터로 받기위해서는 해당 파라매터에 대한 함수 타입을 지정해야한다. 위에서 언급한 max
함수는 다음과 같이 정의된다.
fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
var max: T? = null
for (it in collection)
if (max == null || less(max, it))
max = it
return max
}
파라매터 less
는 (T, T) -> Boolean
타입이다. 즉, 두개의 T
타입 파라매터를 가지고 Boolean
타입을 리턴하며, 첫번째 파라매터가 두번째보다 작은 경우 true 를 리턴한다.
4번째 줄을 보면 less
를 함수로 사용된다. 이 인자는 T
타입의 두개의 인자를 전달해 호출된다.
함수 타입은 위와 같이 작성되거나 각 파라매터의 의미를 문서화하려는 경우 이름 지정 파라매터를 사용해서 작성한다.
val compare: (x: T, y: T) -> Int = ...
람다식 구문
람다식 (함수 타입의 리터럴)의 전체 구문은 다음과 같다
val sum = { x: Int, y: Int -> x + y }
람다식은 항상 중괄호로 둘러 싼다. 전체 구문 형식의 파라매터 선언은 괄호 안에 들어가며, 선택적 타입 Annotation을 가지고, ->
부호 뒤에 본문이 온다. 람다의 반환 타입이 Unit
이 아닌 경우 람다 본문의 마지막 식 (또는 가능한 경우 단일 식)이 반환 값으로 처리된다.
생략 가능한 것을 모두 생략하면 다음과 같다
val sum: (Int, Int) -> Int = { x, y -> x + y }
람다식은 하나의 파라매터를 가지는 것이 일반적이다. Kotlin이 시그니처를 알 수 있다면 파라매터 선언을 생략가능 하고, 암시적 it
이라는 이름으로으로 선언한다.
ints.filter { it > 0 } // 이 이터럴의 타입은 '(it: Int) -> Boolean'
정규 표현식을 사용하여 람다로부터 값을 명시적으로 반환 할 수 있다. 그렇지않으면 마지막 식의 값이 암시적으로 반환된다. 다음 두 스니펫은 동일하다.
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
함수의 마지막 파라매터가 함수면 인자 목록을 갖는 괄호 밖에 람다 식을 전달할 수 있다. callSuffix 문법을 참조
익명 함수
위 람다식 구문에 빠진 하나는 함수의 반환 타입을 지정하는 기능이다. 대부분의 경우 반환 타입을 자동으로 유추하므로 불필요하다. 그러나 명시적으로 지정해야하는 경우 익명 함수 구문을 사용할 수 있다
fun(x: Int, y: Int): Int = x + y
익명 함수는 이름이 생략되어 있는 점을 제외하고 통상적인 함수 선언과 매우 흡사하다. 몸체는 (위 참조) 식이거나 블록일 수 있다
fun(x: Int, y: Int): Int {
return x + y
}
파라매터와 반환 타입은 일반 함수와 같은 방식으로 지정된다. 단, 파라매터 타입을 컨텍스트에서 유추가능 한 경우 생략할 수 있다.
ints.filter(fun(item) = item > 0)
익명 함수에 대한 반환 타입 유추는 일반 함수처럼 동작한다. 반환 타입은 식 본문의 함수의 경우 자동으로 유추되며, 블록 본문의 익명 함수는 명시적으로(은 Unit
으로 가정) 지정해야한다.
익명 함수 파라매터는 항상 괄호안에 전달한다. 괄호 밖에 함수를 위치시킬 수 있는 약식 구문은 람다식만 가능하다.
람다식과 익명 함수의 또 다른 차이점은 비-로컬 리턴의 동작이다. 라벨이 없는 return 문은 항상 fun 키워드로 선언된 함수에서 리턴한다. 즉, 람다식 내부의 return은 둘러싼 함수에서 반환되는 반면 익명 함수 내부의 return은 익명 함수 자체에서 반환된다.
클로저
람다식 혹은익명함수(로컬 함수 및 객체식 뿐만 아니라), 즉 외부 Scope 에서 선언된 변수인 클로저에 접근할 수 있다. Java와는 달리, 클로저에서 캡처된 변수는 수정 가능하다.
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
리시버를 가지는 함수 리터럴
Kotlin은 지정된 리시버 객체로 함수 리터럴을 호출하는 기능을 제공한다. 함수 리터럴의 본문에서 추가 한정자 없이 해당 리시버 객체의 메소드를 호출할 수 있다. 함수의 본문 내부에 있는 리시버 객체의 멤버에 접근할 수 있는 확장 함수와 유사하다. 사용하는 가장 중요한 예 중 하나는 Type-safe Groovy-style builders이다.
이러한 함수 리터럴 타입은 리시버가 있는 함수 타입이다.
sum : Int.(other: Int) -> Int
마치 리시버 객체의 메소드처럼 호출할 수 있
1.sum(2)
익명함수 구문을 사용하면 함수 리터럴의 리시버 타입을 직접 지정할 수 있다. 리시버로 함수 타입의 변수를 선언하고 나중에 사용해야하는 경우 유용하다.
val sum = fun Int.(other: Int): Int = this + other
리시버 타입이 컨텍스트에서 유추될 때 람다식은 리시버와 함께 함수 리터럴로 사용될 수 있다.
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 리시버 객체 생성
html.init() // 리시버 객체를 람다에 전달
return html
}
html { // 리시버가 있는 람다는 여기에서 시작
body() // 리시버 객체에 대한 메소드 호출
}