들어가기 전에
해당 블로깅은 코틀린 공식문서의 High-order functions and lambdas를 번역하며 학습한 내용입니다.
학습중임에 따라 이해하는데 도움이 되는 부분들을 추가되고 의역된 부분이 있습니다. 혹시 잘못된 설명이 있다면 얼마든지 제보해주세요.
코틀린에서 함수는 일급객체(first-class)이므로 변수와 데이터 구조에 저장할 수 있으며, 다른 고차 함수에서 반환될 수 있다. (함수에 함수가 아닌 값에 대해 가능한 모든 작업을 수행할 수 있다.)
이를 용이하게 하기 위해, 코틀린은 정적으로 타입이 지정된 프로그래밍 언어로서 함수를 나타내기 위한 함수 유형 계열을 사용하며, 람다 표현식 같은 특수한 언어 구조를 제공한다.
High-order functions 고차함수
고차함수는 함수를 매개변수로 사용하거나, 함수를 반환하는 함수이다.
고차 함수의 좋은 예는 컬렉션(Collation)에 대한 함수형 프로그래밍 관용구인 fold이다.
초기 누적 값과 결합 함수를 받고, 현재 누적 값과 각 컬렉션 요소를 연속적으로 결합하여 반환 값을 구성하는 함수이며 매번 누적 값을 대체한다.
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
}
위의 코드에서 combine 매개변수는 함수 타입 (R, T) -> R을 가지므로, R과 T 타입의 두 인수를 받아 R 타입의 값을 반환하는 함수를 받을 수 있다. 이 함수는 for 루프 내에서 호출되며, 반환 값은 accumulator에 할당된다.
fold를 호출하려면 함수 유형의 인스턴스를 인수로 전달해야 하며, 람다 표현식은 이를 위해 고차 함수 호출시 널리 사용된다.
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
})
// 람다식에서 매개변수 타입은 추론이 가능하다면 선택사항이다.
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
// 함수 참조는 고차함수에서도 사용 할 수 있다.
val product = items.fold(1, Int::times)
/*
acc = 0, i = 1, result = 1
acc = 1, i = 2, result = 3
acc = 3, i = 3, result = 6
acc = 6, i = 4, result = 10
acc = 10, i = 5, result = 15
joinedToString = Elements: 1 2 3 4 5
product = 120
*/
Function type 함수 유형
Kotlin은 함수에 대한 선언에서 (Int) -> String과 같은 함수 타입을 사용한다. ex) val onClick: () -> Unit = ...
이러한 유형에는 함수의 서명(매개 변수 및 반환 값)에 해당하는 특수 표기법이 있다.
- 모든 함수 유형은 매개변수 유형의 괄호로 묶인 목록과 반환 유형이 있다.
- (A, B) -> C는 A와 B 유형의 두 인수를 취하고 C 유형의 값을 반환하는 함수를 나타내는 유형
매개변수 유형의 목록은 () -> A와 같이 비어 있을 수 있으며, Unit 반환 유형은 생략 불가
- (A, B) -> C는 A와 B 유형의 두 인수를 취하고 C 유형의 값을 반환하는 함수를 나타내는 유형
- 함수 유형은 옵션으로 추가 수신자 유형(receiever type)을 가질 수 있으며, 이는 표기법에서 점 앞에 지정된다.
- A.(B) -> C 유형은 매개 변수 B를 사용하여 수신 객체 A에서 호출할 수 있는 함수를 나타내며 값 C를 반환
수신자가 있는 함수 리터럴은 종종 이러한 유형과 함께 사용된다.
- A.(B) -> C 유형은 매개 변수 B를 사용하여 수신 객체 A에서 호출할 수 있는 함수를 나타내며 값 C를 반환
- Suspending 함수() 는 일시 중지 수정자가 있는 표기법으로 suspend () -> Unit 또는 suspend A.(B) -> C.
함수 유형 표기법에는 함수 매개 변수의 이름이 포함될 수 있다.
(x: Int, y: Int) -> Point. 이러한 이름은 매개 변수의 의미를 문서화하는 데 사용할 수 있다.
함수 형식이 nullable임을 지정하려면 다음과 같이 괄호를 사용한다. ((Int, Int) -> Int)?
함수 타입은 괄호를 사용하여 조합할 수도 있다. (Int) -> ((Int) -> Unit)
화살표 표기법의 괄호 우선순위 (우측이 우선)
(Int) -> (Int) -> Unit = (Int) -> ((Int) -> Unit)
(Int) -> (Int) -> Unit != ((Int) -> (Int)) -> Unit
type alias를 사용하여 함수 형식에 이름을 지정 할 수도 있다.
typealias ClickHandler = (Button, ClickEvent) -> Unit
Instantiating a function type 함수 형식 인스턴스화
함수 형식의 인스턴스를 가져오는 방법은 여러가지가 있다.
- 함수 리터럴 코드 내에서 코드 블록을 다음 형식 중 하나로 사용
- 람다식: { a, b -> a + b }
- 익명함수: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
- receiver가 있는 함수 리터럴은 receiver가 있는 함수 형식의 값으로 사용할 수 있다.
- 기존 선언에 대한 호출 가능한 참조를 사용
- 최상위 함수(top-level), 지역, 맴버, 확장 함수(function) ::isOdd, String::toInt,
- 최상위 속성, 맴버, 지역, 확장 속성(property) List<Int>::size
- 생성자(constructor) ::Regex
- 이들은 특정 인스턴스의 멤버를 가리키는, 바인드 가능한 호출 가능한 참조를 포함: foo::toString.
- 함수 형식을 인터페이스로 구현하는 사용자 지정 클래스의 인스턴스를 사용
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
컴파일러는 충분한 정보가 있는 경우 변수에 대한 함수 형식을 유추 할 수 있다.
val a = { i: Int -> i + 1 } // The inferred type is (Int) -> Int
함수 유형의 non-literal은 receiver가 있는 경우와 없는 경우 상호 교환 가능하여, receiver가 첫 번째 매개 변수 대신 사용될 수 있으며 그 반대도 가능하다.
예를 들어, A.(B) -> C 유형의 값이 예상되는 위치에 (A, B) -> C 유형의 값이 전달되거나 할당될 수 있다. 그리고 그 반대로도 가능하다.
val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK
fun runTransformation(f: (String, Int) -> String): String {
return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK
// result = hellohellohello
receiver가 없는 함수 형식은 변수가 확장 함수에 대한 참조로 초기화된 경우에도 기본적으로 유추된다.
이를 변경하려면 변수 유형을 명시적으로 지정해야 한다.
Invoking a function type instance 함수 형식 인스턴스 호출
함수 타입의 값은 invoke(...) 연산자를 사용하여 호출할 수 있다. f.invoke(x) 혹은 f(x).
만약 값이 receiver 타입을 갖고 있다면, receiver 객체는 첫 번째 인자로 전달되어야 한다.
혹은 값이 확장 함수인 것 처럼 수신사(receiver)객체를 앞에 추가한다. ex) 1.foo(2)
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)) // extension-like call
/*
<-->
Hello, world!
2
3
5
*/
inline functions 인라인 함수
때로는 고차 함수에 대해 유연한 제어 흐름을 제공하는 인라인 함수를 사용하는 것이 좋다.
Lambda expressions and anonymous functions 람다 표현과 익명함수
람다 표현식과 익명함수는 함수 리터럴이다. 함수 리터럴은 선언되지는 않았지만, 표현식으로 즉시 전달되는 함수이다.
max(strings, { a, b -> a.length < b.length })
위 max는 두 번째 인수로 함수 값을 취하기 때문에 고차 함수 이다. 이 두 번째 인수는 그 자체가 함수인 표현식(함수 리터럴)이며 다음 함수와 동일하다.
fun compare(a: String, b: String): Boolean = a.length < b.length
Lambda expression syntax 람다 표현식 구문
람다 식의 구문 형식은 다음과 같다.
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
- 람다 식은 항상 중괄호로 둘러 쌓여있다.
- 전체 구문 형식에서 매개 변수 선언은 중괄호 내부에 있고, 선택적으로 타입 어노테이션(type annotations)을 가진다.
- body영역은 -> 이후에 기재된다.
- 람다의 추론된 반환 타입이 Unit이 아닌 경우, 람다 본문 내에서 마지막(또는 단일) 표현식은 반환 값으로 처리된다.
선택적 어노테이션을 모두 생략하면 다음과 같다.
val sum = { x: Int, y: Int -> x + y }
Passing trailing lambdas 후행 람다 전달
함수의 마지막 매개변수가 함수인 경우, 해당 인수로 전달된 람다 표현식을 괄호 밖에 배치 할 수 있다.
val product = items.fold(1) { acc, e -> acc * e }
이러한 구문을 후행 람다 (trailing lambda)라고 한다.
람다가 해당 호출의 유일한 인수인 경우 괄호를 완전히 생략 할 수 있다.
run { println("...") }
it: implicit name of a single parameter 단일 매개변수의 암시적 이름
람다 식에 매개 변수가 하나만 있는 것은 일반적이다.컴파일러가 매개변수 없이 시그니처를 파싱할 수 있다면, 매개변수를 선언할 필요가 없고 ->를 생략할 수 있다.
이 매개변수는 자동으로 이름이 "it"으로 선언된다.
ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'
Returning a value from a lambda expressions 람다 식에서 값의 반환
정규화된 반환 구문(라벨 테그 return 반환 return@lit)을 사용하여 람다에서 값을 명시적으로 반환할 수 있다.
그렇지 않으면 마지막 식의 값이 암시적으로 반환된다.
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
위 두 구문은 동일하다.이 규칙은 괄호 바깥쪽에 람다 식을 전달하는 것과 함께 LINQ 스타일 코드를 허용한다.
strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }
Underscore for unused variables 사용되지 않는 변수에 대한 밑줄
람다에서 매개변수를 사용하지 않는 경우 이름 대신 밑줄을 넣을 수 있다.
map.forEach { (_, value) -> println("$value!") }
Destructuring in lambdas 람다의 구조화
구조화 선언으로 람다의 구조화를 할 수 있다.
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }
Anonymous functions 익명 함수
위 람다 식에는 함수의 반환 형식을 지정하는것이 누락되어 있다. 대부분의 경우 반환 형식을 자동으로 유추 할 수 있으므로, 반드시 필요하지는 않다. 하지만 명시적으로 지정해야 하는 경우 대체적으로 익명 함수를 사용 할 수 있다.
fun(x: Int, y: Int): Int = x + y
익명함수는 이름이 생략된다는 점을 제외하고는 일반 함수의 선언과 매우 유사하다.
함수의 본문은 표현식 또는 블록 일 수 있다.
fun(x: Int, y: Int): Int {
return x + y
}
매개 변수와 반환 형식은 일반 함수와 동일한 방식으로 지정되지만, 매개 변수 형식은 컨텍스트에서 유추할 수 있는 경우 생략 가능
ints.filter(fun(item) = item > 0)
익명 함수의 반환 타입 추론은 일반 함수와 마찬가지이다.
표현식으로 작성한 익명 함수의 반환 타입은 자동으로 추론되지만, 블록 본문이 있는 익명 함수의 반환 타입은 명시적으로 지정해야 한다.
(혹은 Unit으로 유추)
익명 함수를 매개 변수로 전달할 때는 괄호 안에 배치해야 한다.
함수를 괄호 밖에 둘 수 있는 간편 구문은 람다 식에만 사용 가능
람다 표현식과 익명 함수의 다른 점 중 하나는 non-local returns의 동작이다.
레이블 없이 반환문을 사용하면 항상 fun 키워드로 선언된 함수에서 반환된다.
즉, 람다 표현식 내부에서 반환하면 둘러싸고 있는 함수에서 반환되지만, 익명 함수 내부에서 반환하면 익명 함수 자체에서 반환된다.
Cloures 클로저
람다 식 또는 익명 함수(local function과 object expression fun 도 포함)는 외부 범위에 선언된 변수를 포함하는 클로저에 접근 할 수 있다. 클로저에서 접근한 변수는 람다에서 수정할 수 있다.
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
Function literals with receiver 수신자가 있는 함수 리터럴
A.(B) -> C와 같은 수신자(receiver)가 있는 함수는 특수한 형태로 인스턴스화 할 수 있다.
reveiver 객체를 제공, 이를 사용하여 함수 유형의 인스턴스를 호출하는 기능을 제공한다.
함수 리터럴의 본문에서 호출된 receiver객체는 this 를 통해 추가 수식 없이 receiver 객체 혹은 객체의 맴버에 접근할 수 있다.
이 기능은 확장 함수의 동작과 유사하며, 함수 본문의 receiver객체의 맴버에 접근 할 수도 있다.
receiver유형이 있는 함수 리터럴의 예시이다.
여기서 plus는 receiver객체에서 호출된다.
val sum: Int.(Int) -> Int = { other -> plus(other) }
익명 함수를 사용하면 함수 리터럴의 receiver 형식을 직접 지정할 수 있다.
이는 receiver를 사용하여 함수 유형의 변수를 선언한 다음, 나중에 사용해야 하는 경우에 유용할 수 있다.
val sum = fun Int.(other: Int): Int = this + other
람다 표현식은 receiver 유형을 컨텍스트에서 유추할 수 있는 경우 receiver와 함께 함수 리터럴로 사용할 수 있다.
사용의 가장 중요한 예 중 하나는 type-safe builder 이다.
class HTML {
fun body() { ... }
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // create the receiver object
html.init() // pass the receiver object to the lambda
return html
}
html { // lambda with receiver begins here
body() // calling a method on the receiver object
}
'코틀린 > 코틀린 공식문서' 카테고리의 다른 글
[코틀린 공식문서] Inheritance; 상속 (0) | 2023.08.13 |
---|---|
[코틀린 공식문서] Classes; 클래스 (0) | 2023.08.10 |
[코틀린 공식 문서] Functions; 함수 (1) | 2023.07.27 |
[코틀린 공식문서] Extensions; 확장 (0) | 2023.07.27 |
[코틀린 공식문서] 배열 ; Arrays (0) | 2023.07.27 |