고차 함수와 람다 (High-order functions and lambdas)
코틀린 함수는 퍼스트 클래스 함수로, 변수와 데이터 구조에 저장되며 다른 고차 함수로 전달되고 전달될 수 있습니다. 함수에 대해 다른 비 함수 값에 가능한 모든 작업을 수행할 수 있습니다.
이를 용이하게 하기 위해 정적으로 타입이 지정된 프로그래밍 언어인 Kotlin은 함수를 나타내기 위한 함수 타입 패밀리를 사용하며, 람다 표현식과 같은 특수한 언어 구조를 제공합니다.
고차 함수 (Higher-order functions)
고차 함수는 함수를 매개변수로 받거나 함수를 반환하는 함수입니다.
컬렉션에 대한 함수형 프로그래밍 관용구인 폴드(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 타입의 값을 반환하는 함수를 받을 수 있습니다. 이 함수는 반복문 내에서 호출되며 반환 값은 누산기에 할당됩니다.
fold를 호출하려면 함수 타입의 인스턴스를 매개변수로 전달해야 하며, 이를 위해 람다 표현식(아래에서 자세히 설명)이 고차 함수 호출 지점에서 널리 사용됩니다:
val items = listOf(1, 2, 3, 4, 5)
// Lambdas are code blocks enclosed in curly braces.
items.fold(0, {
// When a lambda has parameters, they go first, followed by '->'
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// The last expression in a lambda is considered the return value:
result
})
// Parameter types in a lambda are optional if they can be inferred:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })
// Function references can also be used for higher-order function calls:
val product = items.fold(1, Int::times)
함수 타입 (Funtion types)
Kotlin은 함수와 관련된 선언에 (Int) -> String과 같은 함수 타입을 사용합니다. 예를 들어, val onClick: () -> Unit = ...와 같이 사용될 수 있습니다.
이러한 타입은 함수의 시그니처에 해당하는 특별한 표기법을 갖고 있습니다. 이는 함수의 매개변수와 반환값을 나타냅니다:
- 모든 함수 타입은 매개변수 타입의 괄호로 둘러싸인 목록과 반환 타입을 갖습니다. (A, B) -> C는 A와 B 타입의 두 인자를 받아 C 타입의 값을 반환하는 함수 타입을 나타냅니다. 매개변수 타입 목록은 비어 있을 수도 있습니다. 예를 들어 () -> 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과는 동일하지 않습니다.
또한 함수 타입에 대체 이름을 부여할 수도 있습니다. 이는 타입 별칭(type alias)을 사용하여 가능합니다:
typealias ClickHandler = (Button, ClickEvent) -> Unit
함수 타입의 인스턴스 생성 (Instantiating a function type)
함수 타입의 인스턴스를 얻는 여러 가지 방법이 있습니다:
- 함수 리터럴 내에서 코드 블록을 사용하는데, 다음 중 하나의 형태를 사용합니다:
- 람다 표현식: { a, b -> a + b },
- 익명 함수: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
리시버를 갖는 함수 리터럴은 리시버를 갖는 함수 타입의 값으로 사용될 수 있습니다.
- 기존 선언에 대한 호출 가능한 참조를 사용합니다:
- 최상위, 지역, 멤버 또는 확장 함수: ::isOdd, String::toInt,
- 최상위, 멤버 또는 확장 프로퍼티: List<Int>::size,
- 생성자: ::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
리시버를 갖는 함수 타입과 그렇지 않은 함수 타입의 리터럴 값은 서로 교환할 수 있습니다. 따라서 리시버는 첫 번째 매개변수 대신에 사용될 수 있고, 그 반대도 가능합니다. 예를 들어, (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
리시버를 갖지 않는 함수 타입은 기본적으로 추론됩니다. 심지어 변수가 확장 함수에 대한 참조로 초기화되더라도 추론됩니다. 이를 변경하려면 변수 타입을 명시적으로 지정하면 됩니다.
함수 타입 인스턴스 호출하기 (Invoking a function type instance)
함수 타입의 값은 invoke(...) 연산자를 사용하여 호출할 수 있습니다: f.invoke(x) 또는 간단히 f(x).
만약 해당 값이 리시버 타입을 가지고 있다면, 리시버 객체는 첫 번째 인자로 전달되어야 합니다. 리시버를 가진 함수 타입의 값을 호출하는 또 다른 방법은 값 앞에 리시버 객체를 붙이는 것인데, 마치 값이 확장 함수인 것처럼: 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
인라인 함수 (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 }
- 람다 표현식은 항상 중괄호로 둘러싸입니다.
- 전체 구문 형식에서 매개변수 선언은 중괄호 내부에 있으며 선택적으로 타입 주석을 가질 수 있습니다.
- 바디는 화살표 (->) 뒤에 옵니다.
- 만약 람다의 추론된 반환 타입이 Unit이 아니라면, 람다 바디 내부의 마지막 (또는 아마도 하나뿐인) 식은 반환 값으로 처리됩니다.
모든 선택적 주석을 생략한다면, 남은 것은 다음과 같이 보입니다:
val sum = { x: Int, y: Int -> x + y }
후행 람다 전달 (Passing trailing lambdas)
Kotlin 관례에 따르면 함수의 마지막 매개변수가 함수인 경우 해당 인수로 전달되는 람다 표현식은 괄호 밖에 배치될 수 있습니다:
val product = items.fold(1) { acc, e -> acc * e }
이러한 구문은 후행 람다로도 알려져 있습니다.
만약 람다가 해당 호출에서 유일한 인수라면, 괄호를 완전히 생략할 수 있습니다:
run { println("...") }
단일 파라미터의 암시적 이름 (it: implicit name of a single parameter)
람다 표현식이 하나의 매개변수만 갖는 것은 매우 흔합니다.
컴파일러가 매개변수 없이 시그니처를 파싱할 수 있는 경우, 매개변수를 선언할 필요가 없으며 ->를 생략할 수 있습니다. 매개변수는 자동으로 "it"이라는 이름으로 암시적으로 선언됩니다.
람다 표현식에서 값 반환하기 (Returning a value from a lambda expression)
라이브러리의 함수에서 람다를 사용할 때, 람다에서 값을 명시적으로 반환할 수 있습니다. 그렇지 않으면 마지막 식의 값이 암시적으로 반환됩니다.
따라서 다음 두 코드 조각은 동등합니다:
ints.filter {
val shouldFilter = it > 0
shouldFilter
}
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
이 관습은 괄호 외부에서 람다 표현식을 전달하며, 이를 통해 LINQ 스타일의 코드를 작성할 수 있습니다:
map.forEach { (_, value) -> println("$value!") }
람다식 내의 구조 분해 (Destructuring in lambdas)
람다식 내의 구조 분해는 구조 분해 선언의 일부로 설명됩니다.
익명 함수 (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 return)의 동작입니다. 레이블이 없는 반환문은 항상 fun 키워드로 선언된 함수에서 반환됩니다. 따라서 람다 표현식 내의 반환은 둘러싸는 함수에서 반환되지만, 익명 함수 내의 반환은 익명 함수 자체에서 반환됩니다.
클로저 (Closures)
람다 표현식이나 익명 함수(또는 로컬 함수와 객체 표현식)은 클로저에 접근할 수 있습니다. 클로저에는 외부 범위에서 선언된 변수들이 포함됩니다. 클로저에 포함된 변수들은 람다 내에서 수정할 수 있습니다:
var sum = 0
ints.filter { it > 0 }.forEach {
sum += it
}
print(sum)
이를 통해 sum과 같은 외부 변수를 람다 표현식 내에서 사용하고 수정할 수 있습니다. 이로써 람다는 자체 범위 외부의 변수들에 접근할 수 있는 강력한 기능을 제공합니다.
리시버를 갖는 함수 리터럴 (Function literals with receiver)
A.(B) -> C와 같은 리시버를 갖는 함수 타입은 함수 리터럴의 특수한 형태인 "리시버를 갖는 함수 리터럴"로 생성할 수 있습니다.
앞서 언급한대로, Kotlin은 리시버 객체를 제공하면서 리시버를 갖는 함수 타입의 인스턴스를 호출할 수 있는 기능을 제공합니다.
함수 리터럴의 본문 내에서 호출에 전달된 리시버 객체는 암시적인 this로 변환되어, 추가적인 한정자 없이 해당 리시버 객체의 멤버에 접근할 수 있습니다. 또는 this 표현식을 사용하여 리시버 객체에 접근할 수 있습니다.
이 동작은 확장 함수의 동작과 유사합니다. 확장 함수도 함수 본문 내에서 리시버 객체의 멤버에 접근할 수 있게 해줍니다.
다음은 리시버를 갖는 함수 리터럴과 해당 타입의 예시입니다. 여기서 plus 메서드가 리시버 객체에서 호출됩니다:
val sum: Int.(Int) -> Int = { other -> plus(other) }
익명 함수 구문을 사용하면 함수 리터럴의 리시버 타입을 직접 지정할 수 있습니다. 이는 함수 타입을 갖는 변수를 선언하고 이후에 사용해야 할 때 유용할 수 있습니다.
val sum = fun Int.(other: Int): Int = this + other
리시버 타입이 컨텍스트에서 추론될 수 있는 경우 람다 표현식을 함수 리터럴로서 리시버를 갖는 함수처럼 사용할 수 있습니다. 그 중 가장 중요한 사용 사례 중 하나는 타입 안전한 빌더(Type-Safe Builders)입니다:
원문
'Kotlin' 카테고리의 다른 글
[Kotlin] Kotlin 공식 문서 번역 - 인라인 value 클래스 (Inline value classes) (0) | 2023.08.12 |
---|---|
[Kotlin] Kotlin 공식 문서 번역 - 인라인 함수 (Inline functions) (0) | 2023.08.12 |
[Kotlin] Extensions 사용법 총 정리 (1) - Extensions란? (0) | 2023.06.18 |
[Kotlin] Sealed Class, Interface 사용법 (1) - Sealed Class, Interface란? (0) | 2023.06.12 |
[Kotlin] 공식 코틀린 코딩 컨벤션 소개 (0) | 2023.06.06 |
댓글