고차 함수

고차 함수

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

  • 함수를 매개변수로 받는 함수

  • 함수를 반환하는 함수

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

객체지향 계산기 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)
}

Last updated