고차 함수와 람다
•
Kotlin 함수는 1급 객체여서 변수에 저장될 수 있고 데이터 구조에 포함될 수 있으며 다른 함수에 인수로 전달되거나 반환 값으로 사용될 수 있다.
•
이걸 가능하게 하기 위해 Kotlin은 함수 타입이라는 타입 시스템을 제공한다.
•
함수 타입은 함수를 표현하기 위한 타입 시스템이며 람다식과 같은 다양한 언어적 구문을 통해 함수를 더 쉽게 다룰 수 있도록 지원한다.
고차 함수
•
고차 함수는 함수를 매개변수로 받거나 함수를 반환하는 함수이다.
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}
Kotlin
복사
•
초기 값(initial)과 결합 함수(combine)를 받아서 초기 값을 누적(accumulator)하고 컬렉션의 각 요소와 결합하여 최종 값을 만들어낸다.
•
combine 매개변수는 (R, T) → R 이라는 함수 타입을 가지고 있고 두 개의 매개변수 R, T를 받아서 R 타입의 값을 반환하는 함수를 나타낸다.
val items = listOf(1, 2, 3, 4, 5)
// 람다는 중괄호로 감싸진 코드 블록입니다.
items.fold(0, {
// 람다의 매개변수는 화살표 '->' 앞에 위치합니다.
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// 람다의 마지막 표현식이 반환 값으로 간주됩니다.
result
})
Kotlin
복사
람다식에서 매개변수 타입 생략
•
람다식에서 매개변수 타입은 컴파일러가 추론할 수 있는 경우 생략할 수 있다.
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
Kotlin
복사
•
acc와 i의 타입이 추론 가능하기 때문에 따로 명시하지 않는다.
함수 참조
•
함수 참조도 고차 함수 호출에 사용할 수 있다.
val product = items.fold(1, Int::times)
Kotlin
복사
•
Int::times는 곱셈을 수행하는 Int 클래스의 times 함수를 combine 인자로 전달한다.
함수 타입
•
함수 타입은 함수의 시그니처(매개변수와 반환값)를 나타낸다.
•
모든 함수 타입은 괄호로 묶인 매개변수 타입 목록과 반환 타입을 가진다.
•
매개변수 타입 목록이 비어있을 수 있으며 () → A 처럼 표시한다.
•
반환 타입이 Unit 일 때는 생략할 수 없다.
•
함수 타입은 수신자 타입을 가질 수 있으며 A.(B) → C 처럼 . 앞에 위치하고 수신자 객체 A에서 호출되고 매개변수 B를 받아 C를 반환하는 함수를 나타낸다.
•
suspend 함수는 suspend () → Unit 이나 suspend A.(B) → C 와 같은 형태로 특별한 함수 타입에 속한다.
•
함수 타입을 표기할 때 매개변수에 이름을 지정할 수 있다. (x: Int, y: Int) → Point
•
함수 타입이 nullable한지 지정하려면 괄호를 사용하여 나타낸다. ((Int, Int) → Int)?
•
함수 타입을 괄호로 묶어 결합할 수 있다. (Int) → ((Int) → Unit)
•
화살표 표기법은 오른쪽 결합성이기 때문에 (Int) → (Int) → Unit 과 ((Int) → (Int)) → Unit 은 다르다
•
typealias를 사용하여 함수 타입에 별칭을 지정할 수 있다.
typealias ClickHandler = (Button, ClickEvent) -> Unit
Kotlin
복사
함수 타입 인스턴스 생성
•
함수 리터럴
◦
람다 표현식
val sum: (Int, Int) -> Int = { a, b -> a + b }
Kotlin
복사
▪
마지막 표현식의 결과가 반환된다.
◦
익명 함수
val multiply: (Int, Int) -> Int = fun(a: Int, b: Int): Int {
return a * b
}
Kotlin
복사
▪
명시적으로 return 키워드를 사용하여 값을 반환해야 한다.
•
호출 가능 참조
◦
함수 참조
fun add(a: Int, b: Int): Int = a + b
val sum: (Int, Int) -> Int = ::add
Kotlin
복사
▪
add 함수를 참조하여 ::add라는 함수 타입 인스턴스를 생성한다.
◦
속성 참조
val lengthGetter: (String) -> Int = String::length
Kotlin
복사
▪
문자열의 length 속성을 참조하여 String::length는 String을 받아 Int를 반환하는 함수 타입 인스턴스를 생성한다.
◦
생성자 참조
val regexConstructor: (String) -> Regex = ::Regex
Kotlin
복사
▪
String을 받아 Regex 객체를 생성하는 ::Regex라는 참조를 통해 함수 타입 인스턴스를 생성한다.
◦
바운드 참조
val foo = "Hello"
val fooToString: () -> String = foo::toString
println(fooToString()) // "Hello" 출력
Kotlin
복사
▪
foo 객체에 바운드된 toString()의 호출을 참조하는 foo::toString 라는 함수 타입 인스턴스를 생성한다.
•
함수 타입을 구현한 클래스의 인스턴스
◦
클래스의 인스턴스를 함수 타입을 인터페이스처럼 사용하는 예
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
Kotlin
복사
▪
IntTransformer 클래스는 (Int) → Int 함수 타입을 구현하며 invoke 연산자를 재정의하여 IntTransformer 객체를 함수처럼 사용할 수 있게 만든다.
•
수신자가 있는 함수 타입과 수신자가 없는 함수 타입의 상호 변환
◦
수신자는 첫 번째 매개변수로 대체될 수 있고 그 반대도 가능하다.
◦
(A, B) → C 타입의 값은 A.(B) → C 타입이 필요한 곳에 전달되거나 할당할 수 있고 그 반대도 가능하다.
val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // 가능
Kotlin
복사
◦
repeatFun은 수신자가 있는 함수 타입 String.(Int) → String이고 이를 수신자가 없는 함수 타입 (String, Int) → String에 할당할 수 있다.
fun runTransformation(f: (String, Int) -> String): String {
return f("hello", 3)
}
val result = runTransformation(repeatFun) // 가능
Kotlin
복사
◦
runTransformation 함수는 (String, Int) → String 타입의 함수 f를 인자로 받아 사용한다.
◦
repeatFun은 수신자 타입이 있는 String.(Int) → String 타입이지만 (String, Int) → String 타입이 요구되는 곳에서도 사용할 수 있다.
•
함수 타입 추론
val a = { i: Int -> i + 1 } // The inferred type is (Int) -> Int
Kotlin
복사
◦
충분한 정보가 있으면 컴파일러는 변수의 함수 타입을 추론할 수 있다.
◦
수신자가 없는 함수 타입은 기본적으로 추론되며 변수를 확장 함수 참조로 초기화해도 동일하게 처리된다.
◦
추론 동작을 변경하려면 변수 타입을 명시적으로 지정해야 한다.
함수 타입 인스턴스 호출
•
함수 타입의 값은 f.invoke(x) 또는 단순히 f(x)의 invoke(…) 연산자를 사용하여 호출할 수 있다.
•
값에 수신자 타입이 있는 경우 수신자 객체를 첫 번째 인수로 전달해야 한다.
•
수신자 타입이 있는 함수 타입 값을 호출하는 또 다른 방법은 해당 값을 수신자 객체 앞에 두는 것이다.
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus
println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!"))
println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // 확장 함수처럼 호출
Kotlin
복사
인라인 함수
•
고차 함수에서 유연한 제어 흐름을 제공하는 인라인 함수를 사용하는 것이 유리할 때가 있다.
람다 표현식과 익명 함수
•
람다 표현식과 익명 함수는 함수 리터럴이다.
•
함수 리터럴은 선언되지 않고 즉시 표현식으로 전달되는 함수이다.
max(strings, { a, b -> a.length < b.length })
Kotlin
복사
•
max() 함수는 고차 함수로 두 번째 인수로 함수 값을 받는다.
•
이 두 번째 인수는 함수 자체인 표현식이며 이는 함수 리터럴로 불린다.
fun compare(a: String, b: String): Boolean = a.length < b.length
Kotlin
복사
•
max() 함수는 compare() 함수처럼 명명된 함수와 동일하다.
람다 표현식 구문
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
Kotlin
복사
•
람다 표현식은 항상 중괄호로 둘러싸여 있다.
•
전체 문법 형식에서 매개변수 선언은 중괄호 안에 위치하며 타입 주석은 선택 사항이다.
•
람다의 본문은 화살표(→) 뒤에 나온다.
•
람다의 추론된 반환 타입이 Unit이 아닌 경우 람다 본문 내부의 마지막 표현식이 반환 값으로 처리된다.
후행 람다 전달
val product = items.fold(1) { acc, e -> acc * e }
Kotlin
복사
•
함수의 마지막 매개변수가 함수인 경우 해당 매개변수에 해당하는 람다 표현식을 괄호 밖에 둘 수 있다.
run { println("...") }
Kotlin
복사
•
만약 람다가 유일한 인수라면 괄호를 완전히 생략할 수 있다.
it : 단일 매개변수의 암시적 이름
ints.filter { it > 0 } // 이 리터럴은 '(it: Int) -> Boolean' 타입입니다
Kotlin
복사
•
람다 표현식에서 매개변수가 하나만 있는 경우 매개변수는 암시적으로 it 이라는 이름으로 선언된다.
•
컴파일러가 시그니처를 매개변수 없이 구문 분석할 수 있으면 매개변수를 선언할 필요가 없으며 화살표(→)도 생략할 수 있다.
람다 표현식에서 값 반환
•
람다에서 값을 명시적으로 반환하려면 한정된 return 구문을 사용해야 한다.
•
그렇지 않으면 마지막 표현식의 값이 암시적으로 반환된다.
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
Kotlin
복사
•
위 두 코드는 동일하다.
strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }
Kotlin
복사
•
이러한 방식과 람다 표현식을 괄호 밖에 전달하는 방식 덕분에 LINQ 스타일의 코드 작성이 가능하다.
사용되지 않는 변수에 대한 밑줄
map.forEach { (_, value) -> println("$value!") }
Kotlin
복사
•
람다 매개변수가 사용되지 않을 경우 변수 이름 대신 밑줄을 사용할 수 있다.
람다에서 구조 분해 선언
•
람다에서의 구조 분해는 구조 분해 선언의 일부로 설명된다.
익명 함수
fun(x: Int, y: Int): Int = x + y
Kotlin
복사
•
익명 함수는 람다 표현식과 비슷하게 사용되지만 반환 타입을 명시적으로 지정할 수 있는 기능을 제공한다.
fun(x: Int, y: Int): Int {
return x + y
}
Kotlin
복사
•
본문은 표현식 또는 블록이 될 수 있고 블록인 경우에는 명시적인 return문을 사용해야 한다.
ints.filter(fun(item) = item > 0)
Kotlin
복사
•
익명 함수의 매개변수 타입과 반환 타입이 추론되는 경우 생략할 수 있다.
// 람다 표현식에서의 return
fun outerFunction() {
val lambda = { return } // 바깥 함수에서 반환됨
lambda()
println("이 코드는 실행되지 않습니다.") // 실행되지 않음
}
// 익명 함수에서의 return
fun outerFunction() {
val anonymousFunc = fun() { return } // 익명 함수 자체에서 반환됨
anonymousFunc()
println("이 코드는 실행됩니다.") // 실행됨
}
Kotlin
복사
•
람다 표현식은 비지역 반환(non-local return)을 지원하지만 익명 함수는 로컬 반환(local return)만 지원한다.
클로저
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
Kotlin
복사
•
람다 표현식이나 익명 함수는 외부 범위에 선언된 변수를 포함하는 클로저를 참조할 수 있다.
•
클로저에서 캡처된 변수는 람다에서 수정할 수 있다.
수신자가 있는 함수 리터럴
val sum = fun Int.(other: Int): Int = this + other
Kotlin
복사
•
A.(B) → C 와 같은 수신자가 있는 함수 타입은 특별한 형태로 인스턴스화 할 수 있다.
•
함수 리터럴의 본문 내부에서 호출된 수신자는 암시적으로 this가 되어 추가적인 한정자 없이 해당 수신자 또는 수신자의 멤버에 접근할 수 있다.
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // 수신 객체 생성
html.init() // 수신 객체를 람다에 전달
return html
}
html { // 수신 객체가 있는 람다가 여기서 시작됩니다
body() // 수신 객체의 메서드 호출
}
Kotlin
복사
•
수신자 타입을 컨텍스트에서 추론할 수 있는 경우 람다 표현식을 수신자와 함께 함수 리터럴로 사용할 수 있다.
•
타입 안전 빌더가 그 예이다.