abstract : 고차 함수의 정의, 부분 함수, 커링 함수, 함수 합성, 응용법

고차 함수

함수형 프로그래밍에서는 아래 두 가지 조건 중 하나 이상을 만족하는 함수를 고차 함수라 한다

  • 함수를 매개변수로 받는 함수
  • 함수를 반환하는 함수

고차함수는 코드의 재사용성을 높임

  • 객체지향 계산기 vs 고차함수 계산기

    • 객체 지향 계산기 : 상속을 사용하며 기능 추가시 중복 코드가 많아짐

      fun main() {
          // OOP 예제
        val calcSum = Sum()
          val calcMinus = Minus()
        val calcProduct = Product()
          val calcTwiceSum = TwiceSum()
      
          println(calcSum.calc(1, 5))     // 6
          println(calcMinus.calc(5, 2))   // 3
          println(calcProduct.calc(4, 2)) // 8
        println(calcTwiceSum.calc(8, 2)) //20
      }
      
      interface Calcable {
          fun calc(x: Int, y: Int): Int
      }
      
      class Sum : Calcable {
          override fun calc(x: Int, y: Int): Int {
              return x + y
          }
      }
      
      class Minus : Calcable {
          override fun calc(x: Int, y: Int): Int {
              return x - y
          }
      }
      
      class Product : Calcable {
          override fun calc(x: Int, y: Int): Int {
              return x * y
          }
      }
      
      class TwiceSum : Calcable {
          override fun calc(x: Int, y: Int): Int {
              return (x + y) * 2
          }
      }
      
    • 고차 함수 계산기 : 비즈니스 기능을 함수로 모듈화

      fun main() {
          // 고차함수를 사용한 예
        val sum: (Int, Int) -> Int = { x, y -> x + y }
          val product: (Int, Int) -> Int = { x, y -> x * y }
          val minus: (Int, Int) -> Int = { x, y -> x - y }
          val twiceSum: (Int, Int) -> Int = { x, y -> (x + y) * 2 }
      
          println(higherOrder(sum, 1, 5))     // 6
          println(higherOrder(minus, 5, 2))   // 3
          println(higherOrder(product, 4, 2)) // 8
          println(higherOrder(twiceSum, 8, 2))   // 20
      }
      
      private fun higherOrder(func: (Int, Int) -> Int, x: Int, y: Int): Int = func(x, y)
      

코드 작성이 간결해짐

  • 입력 리스트의 값을 두 배로 증가시키고 10보다 큰 수를 반환하는 예제
fun main() {
    val ints: List<Int> = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    // 명령형 프로그래밍 예
    val over10Values: ArrayList<Int> = ArrayList()

    for (element in ints) {
        val twiceInt = element * 2
        if(twiceInt > 10){
            over10Values.add(twiceInt)
        }
    }

    println(over10Values)   // [12, 14, 16, 18, 20]

    // 고차함수를 사용한 예
    val result = ints
            .map { it * 2 }
            .filter { it > 10 }

    println(result)            // [12, 14, 16, 18, 20]
}

부분 함수

  • 허용되지 않는 입력값으로 함수를 호출 할 때, 일반적인 프로그래밍에서는 예외를 던지거나 특정값을 리턴하도록 처리

  • 함수형 프로그래밍에서는 이러한 처리를 ‘부분 함수’를 통해 처리

  • 부분 함수란 모든 가능한 입력 중, 일부 입력에 대한 결과만 정의한 함수를 의미

부분 함수의 예

class PartialFunction<in P, out R>(
    private val condition: (P) -> Boolean,
    private val f: (P) -> R)
    : (P) -> R {

    override fun invoke(p: P): R = when {
        condition(p) -> f(p)
        else -> throw IllegalArgumentException("$p isn't supported.")
    }

    fun isDefinedAt(p: P): Boolean = condition(p)
}
  • 부분함수를 사용할 경우 호출하는 쪽에서 호출하기 전 함수가 정상적으로 동작하는지 isDefinedAt과 같은 방법을 제공함으로써 미리 확인 할 수 있다.
  • 호출자가 함수가 던지는 예외나 오류값에 대해서 알지 못하여도 된다.
  • 부분 함수의 조합으로 부분 함수 자체를 재사용 할 수도 있고, 확장 할 수도 있다.

 

부분 적용 함수

  • 부분 적용 함수는 부분 함수와 이름이 비슷하지만 관계는 없음
  • 부분 적용 함수란 전달 받은 매개변수를 가변적으로 사용하여 함수 내부에서 원 함수와 다른 매개변수를 이용하는 함수를 말함
  • 전달하는 매개변수는 가변적임
  • 부분 적용 함수는 코드를 재사용 하기 위해 쓸 수도 있지만, 커링 함수(curried functions)를 구현하기 위해 필요한 개념임
fun main() {
    val func = { a: String, b: String -> a + b }

    val partiallyAppliedFunc1 = func.partial1("Hello")
    val result1 = partiallyAppliedFunc1("World")

    println(result1)  // Hello World

    val partiallyAppliedFunc2 = func.partial2("World")
    val result2 = partiallyAppliedFunc2("Hello")

    println(result2)  // Hello World
}

fun <P1, P2, R> ((P1, P2) -> R).partial1(p1: P1): (P2) -> R {
    return { p2 -> this(p1, p2) }
}

fun <P1, P2, R> ((P1, P2) -> R).partial2(p2: P2): (P1) -> R {
    return { p1 -> this(p1, p2) }
}

 

커링 함수

커링이란 여러 개의 매개변수를 받는 함수를 분리하여, 단일 매개변수를 받는 부분 적용 함수의 체인으로 만드는 방법임

  • 여러 매개변수를 받는 함수

    private fun multiThree(a: Int, b: Int, c: Int): Int = a * b * c
    
  • 한개의 매개변수를 전달받는 체인으로 구성된 커링 함수

    private fun multiThree(a: Int) = { b: Int -> { c: Int -> a * b * c } }
    
  • 두 함수의 호출 결과는 같으나 호출 방법이 다름

        println(partial3) // 6
        println(multiThree(1)(2)(3)) // 6, 함수를 커링으로 쪼갰기 때문에 이러한 형태의 호출이 가능
    

함수형 프로그래밍에서 복잡해 보이는 커링을 사용하는 이유

  • 부분 적용 함수를 다양하게 재사용 할 수 있음
  • 마지막 매개변수가 입력될 때까지 함수의 실행을 늦출 수 있음

코틀린용 커링 함수 추상화하기

코틀린에서는 기본 함수로 커링을 제공하지 않음, 매개변수가 한개인 부분 적용 함수의 체인을 만들기 위해서는 복잡하게 함수를 정의 해야 함 커링을 일반화하여 커링 함수를 쉽게 만들 수 있도록 다음과 같은 방법으로 추상화가 가능함

private fun <P1, P2, P3, R> ((P1, P2, P3) -> R).curried(): (P1) -> (P2) -> (P3) -> R =
        { p1: P1 -> { p2: P2 -> { p3: P3 -> this(p1, p2, p3) } } }

private fun <P1, P2, P3, R> ((P1) -> (P2) -> (P3) -> R).uncurried(): (P1, P2, P3) -> R =
        { p1: P1, p2: P2, p3: P3 -> this(p1)(p2)(p3) }

fun main() {
    val multiThree = { a: Int, b: Int, c: Int -> a * b * c }
    val curried = multiThree.curried()
    println(curried(1)(2)(3))   // 6

    val uncurried = curried.uncurried()
    println(uncurried(1, 2, 3)) // 6
}

 

합성 함수

합성 함수란 고차 함수를 이용해서 두개의 함수를 결합하는 것을 의미함

(f o g)(x) = f(g(x)) 이며 (f o g)(x)는 g 함수가 x를 매개변수로 호출한 결과를 f 함수의 매개변수로 전달한 결과와 같음

infix fun <F, G, R> ((F) -> R).compose(g: (G) -> F): (G) -> R {
    return { gInput: G -> this(g(gInput)) }
}

fun main() {
    println(composed(3))    // 9
}

private fun composed(i: Int) = addThree(twice(i))

private fun addThree(i: Int) = i + 3

private fun twice(i: Int) = i * 2

여러 개의 매개변수를 갖는 함수를 합성하는 방법

import kotlin.math.abs

fun main() {
    val powerOfTwo = { x: Int -> power(x.toDouble(), 2).toInt() }
    val gcdPowerOfTwo = { x1: Int, x2: Int -> gcd(powerOfTwo(x1), powerOfTwo(x2)) }

    println(gcdPowerOfTwo(25, 5))   // 25

    val curriedGcd1 = ::gcd.curried()
    // 잘못된 합성의 예
    val composedGcdPowerOfTwo1 = curriedGcd1 compose powerOfTwo

    println(composedGcdPowerOfTwo1(25)(5))   // 5

    val curriedGcd2 = { m: Int, n: Int -> gcd(m, powerOfTwo(n)) }.curried()
    // 적절한 합성의 예
    val composedGcdPowerOfTwo2 = curriedGcd2 compose powerOfTwo

    println(composedGcdPowerOfTwo2(25)(5))   // 25
}

private tailrec fun gcd(m: Int, n: Int): Int = when (n) {
    0 -> m
    else -> gcd(n, m % n)
}

private tailrec fun power(x: Double, n: Int, acc: Double = 1.0): Double = when (n) {
    0 -> acc
    else -> power(x, n - 1, x * acc)
}

 

실전 응용

zipWith 함수

fun main() {
    val list1 = listOf(6, 3, 2, 1, 4)
    val list2 = listOf(7, 4, 2, 6, 3)

    val add = { p1: Int, p2: Int -> p1 + p2 }
    val result1 = zipWith(add, list1, list2)
    println(result1)    // [13, 7, 4, 7, 7]

    val max = { p1: Int, p2: Int -> max(p1, p2) }
    val result2 = zipWith(max, list1, list2)
    println(result2)    // [7, 4, 2, 6, 4]

    val strcat = { p1: String, p2: String -> p1 + p2 }
    val result3 = zipWith(strcat, listOf("a", "b"), listOf("c", "d"))
    println(result3)    // [ac, bd]

    val product = { p1: Int, p2: Int -> p1 * p2 }
    val result4 = zipWith(product, replicate(3, 5), (1..5).toList())
    println(result4)    // [5, 10, 15]
}

private tailrec fun <P1, P2, R> zipWith(func: (P1, P2) -> R, list1: List<P1>, list2: List<P2>, acc: List<R> = listOf()): List<R> = when {
    list1.isEmpty() || list2.isEmpty() -> acc
    else -> {
        val zipList = acc + listOf(func(list1.head(), list2.head()))
        zipWith(func, list1.tail(), list2.tail(), zipList)
    }
}

콜백 리스너를 고차 함수로 대체

fun main() {
    val result = object : CallBack1 {
        override fun callBack(x1: String): CallBack2 {
            return object : CallBack2 {
                override fun callBack(x2: String): CallBack3 {
                    return object : CallBack3 {
                        override fun callBack(x3: String): CallBack4 {
                            return object : CallBack4 {
                                override fun callBack(x4: String): CallBack5 {
                                    return object : CallBack5 {
                                        override fun callBack(x5: String): String {
                                            return x1 + x2 + x2 + x3 + x4 + x5
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    println(result
        .callBack("1")
        .callBack("2")
        .callBack("3")
        .callBack("4")
        .callBack("5"))     // 12345
}

interface CallBack1 {
    fun callBack(x1: String): CallBack2
}

interface CallBack2 {
    fun callBack(x2: String): CallBack3
}

interface CallBack3 {
    fun callBack(x3: String): CallBack4
}

interface CallBack4 {
    fun callBack(x4: String): CallBack5
}

interface CallBack5 {
    fun callBack(x5: String): String
}