고차함수

코틀린의 고차 함수에 대해 정리한다.

고차 함수

고차 함수란, “다른 함수를 인자로 받거나 반환하는 함수” 이다.

그런데 코틀린에서는, 람다나 함수 참조를 사용해서 함수를 값으로 표현할 수 있다.
그래서, “람다나 함수 참조를 인자로 반거나 반환하는 함수” 도 고차 함수이다.

예를 들어, 표준 라이브러리에 있는 함수인 filter 는 고차 함수이다.
filter 는 술어 함수를 인자로 받는 함수이기 때문이다.

1
2
val numbers = listOf("one", "two", "three", "four")
val longerThan3 = numbers.filter { it.length > 3 } // 술어함수를 인자로 받는 filter

함수 타입

위에서 정리한 것 처럼, 람다를 인자로 받는 함수는 고차함수이다.
그러면 람다 인자의 타입은 어떻게 선언할까 ?

더 단순한 단계인, 람다를 로컬 변수에 대입하는 경우를 보자.

1
2
val sum = { x: Int, y: Int -> x + y }
val action = { println(sum) }

위의 경우, 컴파일러는 sum 과 action 의 함수 타입을 아래 처럼 추론한다.

1
2
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
val action: () -> Unit = { println(sum) }

물론 아래처럼, 널이 될 수 있는 함수 타입을 정의할 수 있다.

1
val funOrNull : ((Int, Int) -> Int)? = null

인자로 받은 함수 호출

이제, 고차 함수를 구현하는 방법을 보자.

1
2
3
4
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("result is : $result")
}

인자로 받은 함수를 호출하는 구문은, 일반 함수를 호출하는 구문과 같다.
그리고, 위 고차함수를 호출할 때는 아래 처럼 람다를 전달하면 된다.

1
2
twoAndThree { a, b -> a + b }
twoAndThree { a, b -> a * b }

디폴트 값 지정

파라미터를 함수 타입으로 선언할 때, 디폴트 값을 지정할 수 있다.

다음 예시를 보자.
toString() 을 사용해서 객체를 문자열로 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(separator)
} else {
result.append(element) // HERE !!
}
}
result.append(postfix)

return result.toString()
}

toString() 을 디폴트 값으로 지정하고, 호출하는 쪽에서 지정하게 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() } // HERE !!!
): String {
val result = StringBuilder(prefix)

for ((index, element) in this.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(transform(element)) // HERE !!!

}
result.append(postfix)

return result.toString()
}

호출하는 쪽에서서 joinToString() 에 아무 값도 전달하지 않으면,
default 로 정의한 변환 함수를 사용한다.

1
2
3
val letters = listOf("A", "B", "C")
val result2 = letters.joinToString()
val result1 = letters.joinToString { it.toLowerCase() }

함수를 함수에서 반환

“다른 함수를 반환하는” 함수를 정의하려면,
반환 타입으로 함수 타입을 지정하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum class Delivery {
STANDARD, EXPEDITED
}

class Order(val itemCount: Int)

fun getShippingCostCalculator(
delivery: Delivery
): (Order) -> Double { // HERE !!!
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount }
}
return { order -> 1.2 * order.itemCount }
}

fun main() {
val shippingCostCalculator = getShippingCostCalculator(Delivery.EXPEDITED)
val cost = shippingCostCalculator(Order(3))
println(cost)
}

getShippingCostCalculator 함수는
Order 를 받아서 Double 을 반환하는 함수를 반환한다.

람다를 활용한 중복 제거

코드 중복을 줄이기 위해, 함수 타입을 사용할 수 있다.

웹 사이트의 방문 기록을 분석하는 예를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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("/login", 10.0, OS.ANDROID),
SiteVisit("/signup", 12.0, OS.IOS),
SiteVisit("/", 20.0, OS.MAC),
SiteVisit("/", 40.1, OS.LINUX),
)

윈도우 사용자의 평균 방문 시간을 구하자.

1
2
3
4
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()

맥 사용자의 평균 방문 시간을 구하자.

1
2
3
4
val averageMacDuration = log
.filter { it.os == OS.MAC }
.map(SiteVisit::duration)
.average()

중복 코드를 피하기 위해,
확장 함수로 정의해서 다음과 같이 개선할 수 있다.

1
2
3
4
fun List<SiteVisit>.averageDuration(os: OS) = 
filter { it.os == os }.map(SiteVisit::duration).average()

log.averageDuration(OS.MAC)

만약에, 다음과 같은 복잡한 질의에 대해 분석하고 싶을 때는 어떻게 할까 ?
“ANDROID 사용자의 /login 페이지 평균 방문 시간은 ? “

이 때, 람다를 적용할 수 있다.

1
2
3
4
fun List<SiteVisit>.averageDuration(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()

log.averageDuration { it.os == OS.ANDROID && it.path == "/login" }

Kotlin In Action <드미트리 제메로프, 스베트라나 이사코바>

Comments