본문 바로가기

함수형 프로그래밍

[함수형 프로그래밍] 고차 함수

powered by Chat GPT

 

안녕하세요.

오늘은 함수형 프로그래밍에 필요한 기본 개념인 고차 함수에 대해 알아보도록 하겠습니다.

  1. 고차 함수 (High-Order Function)
  2. 부분 함수 (Partial Function)
  3. 커링 함수 (Currying Function)
  4. 합성 함수 (Composition Function)

일급 객체

함수형 프로그래밍에서는 함수를 객체처럼 다룹니다.

코틀린도 이러한 함수형 프로그래밍 기능을 지원하는 언어 중 하나입니다.


1) 고차 함수 (High-Order Function)

함수를 변수처럼 전달하고 반환할 수 있는 개념으로, 아래 2가지 기능이 가능합니다

  • 함수를 매개변수로 받는 함수 ( highOrderFunction1 )
  • 함수를 반환하는 함수 ( highOrderFunction2 )
fun highOrderFunction1(func : () -> Unit) {
    func()
}

fun highOrderFunction2() : () -> Unit {
    return { println("Hello World") }
}

장점

고차 함수를 사용함으로써 아래 이점을 얻을 수 있습니다.

 

1) 재사용성

Before - 상속을 사용한 예제

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
    }
}

fun main() {
    val calcSum = Sum()
    val calcMinus = Minus()
    val calcProduct = Product()

    println(calcSum.calc(1, 5)) // 6
    println(calcMinus.calc(5, 2)) // 3
    println(calcProduct.calc(4, 2)) // 8
}

 

After

Override fun calc와 같은 boilerplate 코드가 줄어듭니다

fun highOrder(func: (Int, Int) -> Int, x: Int, y: Int): Int = func(x, y)

fun main() {
    val sum: (Int, Int) -> Int = { x, y -> x + y}
    val minus: (Int, Int) -> Int = { x, y -> x - y}
    val product: (Int, Int) -> Int = { x, y -> x * y}

    println(highOrder(sum, 1, 5)) // 6
    println(highOrder(minus, 5, 2)) // 3
    println(highOrder(product, 4, 2))// 8
}

 

2) 간결성

Before - 명령형 프로그래밍

fun main() {
    val ints = 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]
}

 

After

fun main() {
    val ints = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    val result = ints
        .map { it * 2 }
        .filter { it > 10 }

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

2) 부분 함수 (Partial Function)

일부 입력에 대해서만 정의된 함수입니다.

→ 입력 값이 특정 조건을 만족할 때만 결과를 반환 / 그렇지 않으면 실행되지 않는 함수

// 일반 함수 (모든 입력에 대해 정의됨)
fun square(x: Int): Int = x * x

// 부분 함수 (양수에 대해서만 정의됨)
fun positiveSquare(x: Int): Int? {
    return if (x >= 0) x * x else null
}

fun main() {
    println(square(4))    // 16
    println(positiveSquare(4))  // 16
    println(positiveSquare(-3)) // null (정의되지 않음)
}

 

활용

condition → 함수가 특정 값에 정의되어 있는지 확인하는 함수

applyIfDefined → 정의된 경우에만 실행하는 함수

class PartialFunction<in P, out R>(
    private val condition: (P) -> Boolean,
    private val apply: (P) -> R
) {
    fun isDefined(value: P): Boolean = condition(value)

    fun applyIfDefined(value: P): R? = when {
        condition(value) -> apply(value)
        else -> null
    }
}

fun main() {
    val positiveSquare = PartialFunction<Int, Int>(
        condition = { it >= 0 },
        apply = { it * it }
    )

    println(positiveSquare.applyIfDefined(4)) // 16
    println(positiveSquare.applyIfDefined(-3)) // null
}

 

필요성

가장 좋은 방법은 부분 함수를 만들어야 하는 상황을 만들지 않는 것입니다.

  • 함수형 프로그래밍에서 함수를 만들 때는 가급적 모든 입력에 대한 결과를 정의하는 것이 좋습니다

 

물론 불가피하게 작성해야 하는 경우도 있습니다.

  • 리스트의 첫번째나 마지막 값을 꺼내는 함수에서 빈 리스트에 접근했을 때 처리하는 방법
  • 이 같은 경우에 부분 함수를 활용합니다

3) 커링 함수 (Currying Function)

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

  • 다중 인자 함수 → 일련의 단일 인자 함수로 바꾸는 과정
  • change f(a, b, c) to f(a) → f(b) → f(c)

 

+) 부분 적용 함수

여러 개의 매개변수를 받는 함수에서 일부 인자만 미리 적용하여 새로운 함수를 만드는 방법

fun multiply(a: Int, b: Int) = a * b

fun main() {
    // 'a' 값을 2로 고정한 부분 적용 함수
    val double = { x: Int -> multiply(2, x) }

    println(double(5))  // 10
    println(double(10)) // 20
}

 

Currying 함수 예제

operation(5) → b를 받는 함수 반환

operation(5)(3) → op를 받는 함수 반환

operation(5)(3)(Int::plus) → 최종 계산 수행

fun operation(a: Int) = { b: Int -> { op: (Int, Int) -> Int -> op(a, b) } }

fun main() {
    val addWith5 = operation(5)(3)(Int::plus)  // 5 + 3 = 8
    val multiplyWith4 = operation(4)(2)(Int::times) // 4 * 2 = 8

    println(addWith5)      // 8
    println(multiplyWith4) // 8
}

4) 합성 함수 (Composition Function)

두 개 이상의 함수를 결합하여 새로운 함수를 만드는 기법

(f ∘ g)(x) = f(g(x))

 

예제)

1- 일반적인 예제

fun double(x: Int) = x * 2
fun square(x: Int) = x * x

fun main() {
    val composedFunction = { x: Int -> square(double(x)) }
    println(composedFunction(3)) // (3 * 2) ^ 2 = 36
}

 

2- 확장 함수 예제

infix fun <A, B, C> ((B) -> C).compose(other: (A) -> B): (A) -> C {
    return { x: A -> this(other(x)) }
}

fun main() {
    val double = { x: Int -> x * 2 }
    val square = { x: Int -> x * x }

    val composed = square compose double  // f ∘ g
    println(composed(3)) // (3 * 2) ^ 2 = 36
}

포인트 프리 스타일

함수를 정의할 때 입력 값을 직접 명시하지 않고, 함수 조합만으로 표현하는 방식을 의미합니다.
즉, 함수의 인자를 생략하고 함수의 조합만으로 원하는 로직을 구현하는 방식입니다.

infix fun <A, B, C> ((B) -> C).compose(other: (A) -> B): (A) -> C {
    return { x: A -> this(other(x)) }
}

fun main() {
    val absolute = { i: List<Int> -> i.map { it -> abs(it) }}
    val negative = { i: List<Int> -> i.map { it -> -it }}
    val minimum = {i: List<Int> -> i.min() }

    val composed = minimum compose negative compose absolute
    val result = composed(listOf(3, -1, 5, -2, -4, 8, 14))
    println(result) // -14
}

참고

코틀린으로 배우는 함수형 프로그래밍 4장 고차함수

https://www.yes24.com/product/goods/84899008

 

코틀린으로 배우는 함수형 프로그래밍 - 예스24

차세대 언어 코틀린을 이용해실전에서 활용할 수 있는 함수형 코드를 설계한다!어렵게만 느껴지는 함수형 개념을 충실히 설명하여, 실전에서 활용할 수 있는 지식이 될 수 있도록 한 책이다. 다

www.yes24.com