Language/Kotlin

[Kotlin] Higher-order Functions (고차 함수)

Junhong Kim 2023. 7. 2. 22:13
728x90
반응형

고차 함수란?

고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수를 의미합니다. 

  1. 람다나 함수 참조를 인자로(argument)로 넘길 수 있으면 고차 함수이다.
    • 예: 표준 라이브러리 함수인 filter는 술어 함수(predicate function)를 인자로 받으므로 고차 함수이다.
      • 술어 함수란? 반환 값으로 진리 값(참/거짓)을 반환하는 함수
  2. 람다나 함수 참조를 반환하면 고차 함수이다.

함수 타입

람다를 인자로 받는 함수를 정의하기 위해서는 람다 인자의 타입을 선언하는 방법에 대해 먼저 알아야합니다. 코틀린의 타입 추론으로 인해 변수 타입을 지정하지 않아도 람다를 변수(variable)에 대입할 수 있으므로 다음과 같이 정의할 수 있습니다.

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

 

 

만약, 각 변수에 구체적인 타입 선언을 추가하려면 다음과 같이 정의합니다.

val sum: (Int, Int) -> Int = { x, y -> x + y } // x와 y의 파라미터 타입이 생략됨
val action: () -> Unit = { println(42) }

즉, 함수 타입을 정의하려면 함수 파라미터의 타입을 괄호 안에 넣고 화살표 뒤에 함수의 반환 타입을 지정하면 됩니다. 일반 함수를 정의할 때는 Unit 반환 타입 지정을 생략해도 되지만, 함수 타입을 선언할 때는 반환 타입을 반드시 명시해야 하므로 Unit도 명시해야합니다.

 

이처럼 변수 타입을 함수 타입으로 지정하면 함수 타입에 있는 파라미터로 람다의 파라미터 타입을 유추할 수 있므로 람다 식 안에서 파라미터 타입을 생략해도 됩니다.

인자로 받은 함수 호출

다음 예제는 간단한 고차 함수를 구현하여 인자로 받은 함수를 내부에서 연산을 위해 사용하고 그 결과를 출력합니다. 인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 같습니다.

fun twoAnThree(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

디폴트 값을 지정한 함수 타입

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

다음 예제를 통해서 어떻게 디폴트 값을 지정하고 활용할 수 있는지 알아봅니다.

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

>> val letters = listOf("Alpha", "Beta")
>> println(letters.joinToString { it.toLowerCase() })
Alpha, Beta

>> val letters = listOf("Alpha", "Beta")
>> println(letters.joinToString(separator = "! ", postfix = "! ", transform = {it.toUpperCase() }))
Alpha! Beta!

함수 타입에 대한 티폴트 값을 선언시 특별한 구문이 필요하지 않으며 다른 디폴트 파라미터 값과 마찬가지로 함 수타입에 대한 디폴트 값 선언을 `=` 뒤에 람다를 선언하면 됩니다. 위 예제는 함수 타입의 디폴트 값을 사용한 예제와 함수 타입 직접 전달한 예제를 보여줍니다.

함수를 함수에서 반환

프로그램의 상태나 다른 조건에 따라 달라질 수 있는 로직이 있는 경우 함수가 함수를 반환할 수도 있습니다. 예를 들어, 다음 예제에서는 사용자가 선택한 배송 수단에 따라 배송비를 계산하는 방법이 달라지게 된다면, 이때 적절한 로직을 함수로 반환하는 함수를 정의할 수 있습니다.

enum class Delivery { STANDARD, EXPEDITED } // 표준, 더 신속히 처리

class Order(val itemCount: Int)

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

---

>> val calculator = getShippingCostCalculator(Delivery.EXPEDITED) // 반환받음 함수 변수에 저장
>> println("Shipping costs ${calculator(Order(3))}") // 반환받은 함수를 호출
Shipping costs 12.3

 

다른 함수를 반환하는 함수를 정의하려면 함수의 반환 타입으로 함수 타입을 지정하면 됩니다. 위 예제에서는 Order를 받아서 Double을 반환하는 함수를 반환하고 있으며, 함수를 반환하려면 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("/signup", 8.0, OS.ISO),
  SiteVisit("/", 16.3, OS.ANDORID),
)

사이트 방문 데이터를 하드 코딩한 필터를 사용해서 구현해봅니다.

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

---
>> println(averageWindowsDuration)
23.0

다른 OS 사용자의 통계를 구하고 싶은 경우 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

기존 함수를 확장 함수로 정의하여 가독성을 높일 수 있습니다. 만약, 위 함수가 특정 함수 내부에서만 쓰인다면 이를 로컬 확장 함수로 정의할 수도 있습니다.

 

다만, 이 함수는 다른 의미를 갖는 사용자 (예로 모바일 디바이스 사용자)의 평균 방문 시간을 구하거나 더 복잡한 질의를 사용해 방문 기록을 분석하기에는 알맞지 않습니다. 이때 람다를 사용해서 함수 타입을 사용하면 필요한 조건을 파라미터로 추출할 수 있습니다. 술어 함수를 사용하는 고차 함수를 만들면 다음과 같이 사용할 수 있습니다.

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") })
8.0

위와 같은 코드 중복을 줄일 때 함수 타입이 큰 도움이 됩니다. 또한, 코드의 일부분을 람다로 만들면 중복을 제거할 수 있습니다. 변수, 프로퍼피, 파라미터 등을 사용해 데이터의 중복을 없앨 수 있는 것처럼 람다를 사용하면 코드의 중복을 없앨 수 있습니다.

참고

Kotlin in Action - 드미트리 제메로프 , 스베트라나 이사코바 저자(글) · 오현석 번역

https://kotlinlang.org/docs/lambdas.html#higher-order-functions

728x90
반응형