고차 함수 정의
고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수이다.
코틀린은 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있으므로 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나 함수 참조를 반환하는 함수인 것이다.
ex) filter 함수는 술어 함수를 인자로 받으므로 고차 함수이다.
list.filter(x > 0)
함수 타입
// 타입 추론
val sum = {x: Int, y: Int -> x + y}
val action = { println(42) }
// 함수 타입 선언
val sum: (Int, Int) -> Int = {x, y -> x + y}
val action: () -> Unit = { println(42) }
함수 타입을 선언할 때는 반환 타입을 반드시 명시해야 한다. 위 코드에서의 Unit도 마찬가지이다.
변수 타입을 함수 타입으로 지정하면 함수 타입에 있는 파라미터로부터 람다의 파라미터 타입을 유추할 수 있으므로, 람다 식 안에서 파라미터 타입을 생략할 수 있다.
// 반환 타입이 널이 될 수 있는 타입
val canReturnNull: (Int, Int) -> Int? = {x, y -> Null}
//함수 타입 전체가 널이 될 수 있는 타입
var funOrNull: ((Int, Int) -> Int)? = null
함수 타입 또한 반환 타입을 널이 될 수 있는 타입으로 지정 가능하다.
또한 함수 타입 변수가 널이 될 수 있는 타입으로도 정의할 수 있다.
하지만 함수의 반환 타입이 아닌 함수 타입 전체가 널이 될 수 있는 타입을 선언하기 위해 함수 타입을 괄호로 감싸고, 뒤에 물음표를 붙인다.
단지 괄호의 유무 차이일 뿐이지만, 이에 따라 널이 될 수 있는 함수 타입이 아닌 널이 될 수 있는 반환 타입을 갖는 함수 타입이 선언되기도 한다.
- 파라미터 이름과 함수 타입
파라미터 이름은 타입 검사 시 무시된다. 이 함수 타입의 람다를 정의할 때 파라미터 이름이 꼭 함수 타입 선언의 파라미터 이름과 일치하지 않아도 되지만, 함수 타입에 인자 이름을 추가하면 코드 가독성이 좋아지고, IDE는 그 이름을 코드 완성에 사용할 수 있다.fun performRequest(url: String, callback: (code: Int, content:String) -> Unit) { /*. . . .*/ } >>> val url = "http://kotl.in" >>> performRequest(url) { code, content -> /*. . . .*/ } >>> performRequest(url) { code, page -> /*. . . .*/ }
인자로 받는 함수 호출
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
>>> twoAndThree { a, b -> a + b }
The result is 5
>>> twoAndThree { a, b -> a * b }
The result is 6
인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 같다.
예제를 보기 위해 filter 함수를 구현해보자.
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
for (index in 0 until length) {
val element = get(index)
if (predicate(element)) sb.append(element)
}
return sb.toString()
}
>>> println("ab1c".filter {it in 'a'..'z'})
abc
filter는 문자열의 각 문자를 술어에 넘겨 반환 값이 true면 결과를 담는 StringBuilder 뒤에 문자를 추가한다.
자바에서 코틀린 함수 타입 사용
컴파일된 코드 안에서 함수 타입은 일반 인터페이스로 바뀐다.
즉 함수 타입의 변수는 FunctionN 인터페이스를 구현하는 객체를 저장한다.
- 코틀린 표준 라이브러리는 함수 인자의 개수에 따라 Function()<R>(인자가 없는 함수),
Function1<P1, R>(인자가 하나인 함수)등의 인터페이스를 제공한다. - 각 인터페이스에는 invoke 메소드 정의가 하나 들어있다. invoke를 호출하면 함수를 사용할 수 있으며,
함수 타입인 변수는 인자 개수에 따라 적당한 FunctionN 인터페이스를 구현하는 클래스의 인스턴스를 저장, 그 클래스의 invoke 메소드 본문에는 람다의 본문을 저장한다.
/* 코틀린 선언 */
fun processTheAnswer(f: (Int) -> Int) {
println(f(42))
}
/* 자바 */
>>> processTheAnswer(number -> number + 1);
43
자바 8 이전의 자바에서는 필요한 FunctionN 인터페이스의 invoke 메소드를 구현하는 무명 클래스를 넘기면 된다.
>>> processTheAnswer {
... new Function1<Integer, Integer>() {
... @override
... public Integer invoke(Integer number) {
... System.out.println(number);
... return number + 1;
... }
...});
43
자바에선 코틀린 표준 라이브러리가 제공하는 람다를 인자로 받는 확장 함수를 쉽게 호출할 수 있지만, 수신 객체를
확장 함수의 첫 번째 인자로 명시적으로 넘겨야 하여 코틀린에서 확장 함수를 호출할 때처럼 코드가 깔끔하지 않다.
>>> List<String< strings = new ArrayList();
>>> strings.add("42")
>>> CollectionsKt.forEach(strings, s -> {
... System.out.println(s);
... return Unit.INSTANCE;
...});
코틀린의 Unit 타입은 값이 존재하기 때문에 자바에서 그 값을 명시적으로 반환해야 한다.
(String) -> Unit처럼 변환 타입이 Unit인 함수 타입의 파라미터 위치에 void를 반환하는 자바 람다를 넘길 수 없는 것이다.
디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터
파라미터를 함수 타입으로 선언할 때도 디폴트 값을 정할 수 있다.
fun <T> Collection<T>.jointoString {
separator: String = ", ",
prefix: String = "",
postfix: String = ""
}: String {
val result = StringBuilder(prefix)
for((index, element) in this.withIndex()) {
if(index > 0) result.append(seperator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
여기에는 컬렉션의 각 원소를 문자열로 반환하는 방법을 제어할 수 없다는 단점이 있다.
이는 함수 타입의 파라미터에 대한 디폴트 값을 지정하여 해결할 수 있다.
fun <T> Collection<T>.joinToString(separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() } // 함수 타입 파라미터를 선언하면서 람다를 디폴트 값으로 지정
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element))
}
result.append(postfix)
return result.toString()
}
>>> val letters = listOf("Alpha", "Beta")
>>> println(letters.joinToString())
Alpha, Beta
>>> println(letters.joinToString{ it.toLowerCase() })
alpha, beta
>>> prinln(letters.joinToString(seperator = "! ", postfix = "! ", transform = { it.toUpperCase() }))
ALPHA! BETA!
다른 접근 방법으로는 널이 될 수 있는 함수 타입을 사용할 수 있다.
하지만 널이 될 수 있는 함수 타입으로 함수를 받으면 그 함수를 직접 호출할 수 없다.
코틀린이 NPE가 발생할 가능성이 있어 컴파일을 거부하기 때문이다.
이때 null 여부를 명시적으로 검사를 하면 된다.
fun foo(callback: (() -> Unit)?) {
// ...
if(callback != null) {
callback()
}
}
함수 타입이 invoke 메소드를 구현하는 인터페이스이므로 이를 활용하여 더 짧게 만들 수 있다.
fun foo(() -> Unit)?) {
// ...
callback?.invoke()
}
함수를 함수에서 반환
enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)
fun getShippingCostCaculator(
delivery: Delivery
) : (Order) -> Double {
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount }
}
return { order -> 1.2 * order.itemCount }
}
>>> val calculator = getShippingCostCaculator(Delivery.EXPEDITED)
>>> println("Shipping costs ${calculator(Order(3))}")
Shipping costs 12.3
다른 함수를 반환하는 함수를 정의하려면 함수의 반환 타입으로 함수 타입을 지정해야 한다.
함수를 반환하려면 return 식에 람다나 멤버 참조, 함수 타입의 값을 계산하는 식 등을 넣으면 된다.
또한 문자열과 같은 일반 타입의 값을 함수가 쉽게 반환할 수 있는 것처럼 함수 타입을 사용하면 함수에서 함수를 쉽게 반환할 수 있다.
람다를 활용한 중복 제거
람다를 사용할 수 없는 환경에선 아주 복잡한 구조를 만들어야만 피할 수 있는 코드 중복도 람다를 활용한다면 간결하고 쉽게 중복을 제거할 수 있다.
웹사이트 방문 기록을 분석하는 예를 보도록 하자.
data class SiteVisit {
val path: String,
val duration: Double,
val os: OS
}
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/singup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
윈도우 사용자의 평균 방문 시간을 출력하고 싶다 하면 다음과 같다.
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
>>> println(averageWindowsDuration)
23.0
이제 맥 사용자의 평균 방문 시간을 출력할 것인데, 중복을 피하기 위해 OS를 파라미터로 뽑아낸다.
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os}.map(SiteVisit::duration).average()
>>> println(log.averageDurationFor(OS.WINDOWS))
23.0
>>> println(log.averageDurationFor(OS.MAC))
22.0
함수를 확장으로 정의함으로써 가독성이 좋아진 것을 볼 수 있다.
이 함수가 어떤 함수 내부에서만 쓰인다면 로컬 확장 함수로 정의할 수 있다.
모바일 디바이스 사용자(IOS, ANDROID)의 평균 방문 시간을 구하고 싶다면 다음과 같이 해야 한다.
val averageMobileDuration = log
.filter { it.os in setOf(OS.IOS, OS.ANDROID) }
.map(SiteVisit::duration)
.average()
>>> println(averageMobileDuration)
12.15
플랫폼을 표현하는 간단한 파라미터로는 이런 상황을 처리할 수 없어 하드 코딩한 필터를 사용해야 한다.
"IOS 사용자의 /signup 페이지 평균 방문 시간?"과 같은 더 복잡한 질의를 사용해야 한다면 람다를 사용하면 된다.
함수 타입을 사용하면 필요한 조건을 파라미터로 뽑아낼 수 있다.
fun List<SiteVisit>.averageDurationFor(predicate: <SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
>>> println(log.averageDurationFor {
... it.os in setOf(OS.ANDROID, OS.IOS) })
12.15
>>> println(log.averageDurationFor {
... it.os == OS.IOS && it.path == "/signup"})
6.0
코드의 일부분을 복사해 붙여 넣고 싶은 경우가 있다면 그 코드를 람다로 만들면 중복을 제거할 수 있다.
변수, 프로퍼티, 파라미터 등을 사용해 데이터의 중복을 없앨 수 있는 것과 같이 람다를 사용해서 코드의 중복을 제거할 수 있다.
'Programming > Kotlin' 카테고리의 다른 글
코틀린(Kotlin) - 고차 함수 안에서 흐름 제어 (0) | 2021.11.14 |
---|---|
코틀린(Kotlin) - 인라인 함수 (0) | 2021.11.12 |
코틀린(Kotlin) - 연산자 오버로딩과 기타 관례 (1) | 2021.10.31 |
코틀린(Kotlin) - 컬렉션과 배열 (0) | 2021.10.24 |
코틀린(Kotlin) - 코틀린의 원시 타입 (0) | 2021.10.21 |
댓글