본문 바로가기

IT/코틀린으로 배우는 함수형 프로그래밍

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

안녕하세요 남갯입니다.

 

함수형프로그래밍에서는 함수를 객체처럼 다룬다. 고차함수를 통해 함수를 더 유연하게 다루는 방법에 대해 알아보고

고차함수의 장점을 살펴보자.

 

고차함수란?

코틀린의 함수들은 함수를 인자로 받거나 반환값으로 반환하는 것이 가능하다. 코틀린에서 함수는 일급함수이기 때문.

 

고차함수의 조건

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

- 함수를 반환하는 함수

 

명령형 언어는 문제를 해결하기 위해 상태를 변경 혹은 반복문을 사용하여 단계별 정의하지만

함수형 언어에서는 문제를 해결할 때 반드시 고차 함수를 사용해야 한다.

 

    fun highOrderFunction1(func : () -> Unit) : Unit {
        func()
    }

    fun highOrderFunction2() : () -> Unit{
        return {println("hello world") }
    }

위 두개를 만족하는 함수는 고차함수이다.

 

고차함수를 이용하면 좀 더 짧은 코드를 만들 수 있음.

 

기능의 확장

 

fun calcu() {
        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 }
}
    
fun highOrder(func: (Int, Int) -> Int, x: Int, y: Int): Int = func(x, y)

highOrder라는 함수를 이용하면 함수의 확장이 쉬워진다.

 

 

코드를 간결하게 작성가능

    val result = ints.map { value -> value * 2 }.filter { value -> value > 10 }

 

map과 filter가 함수를 인자로 받는 고차함수이기 때문에 가능하다.

 

부분함수

부분함수란?

모든 가능한 입력중 일부입력에 대한 결과만 정의한 함수를 의미한다.

 

 

fun twice(x: Int) = x * 2
fun partialTwice(x: Int): Int =
    if (x < 100) {
        x * 2
    } else {
        throw IllegalArgumentException()
    }

twice는 두배를 만들어주는거지만

partialTwice 의 두배함수에서의 부분집합이다.

 

fun sayNumber1(x: Int): String = when (x) {
        1 -> "One!"
        2 -> "Two!"
        3 -> "Three!"
        else -> "Not Between 1 and 3"
    }

    fun sayNumber2(x: Int): String = when (x) {
        1 -> "One!"
        2 -> "Two!"
        3 -> "Three!"
        else -> throw IllegalArgumentException()
    }

sayNumber2는 sayNumber1의 부분집합이다.

 

 

부분함수 만들기

스칼라나 함수형 언어에서는 부분함수를 만들기 위한 추상화된 클래스를 제공한다.

하지만 코틀린은 언어적인 차원에서 제공하지 않아서 만들어보자

 

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

 

p의 컨디션이 맞으면 f함수가 실행되고 입력한 조건이 맞는지 확인하는 isDefinedAt함수가 제공된다.

 

    fun partTest(){
        val oneTwoThree = PartialFunction(condition,body)
        if(oneTwoThree.isDefinedAt(3)){
            print(oneTwoThree(3))
        }else{
            print("isDefinedAt(x) return false")
        }
    }

 

짝수인지 확인하는 함수

 

fun evenTesT(){
        val isEven = PartialFunction<Int,String>({it % 2 == 0}, {"$it is even"})
        if(isEven.isDefinedAt(100)){
            print(isEven(100))
        }else{
            print("false")
        }
    }

 

fun <P,R> ((P) -> R).toPartialFunction(definedAt : (P) -> Boolean) : PartialFunction<P,R> = PartialFunction(definedAt,this)

확장함수를 통해 위와같이 만들면

 

body.toPartialFunction(condition)

 

와 같이 가능

 

부분함수의 필요한 이유

1. 호출하는족에서 호출하기 전에 함수가 정상적으로 동작하는지 미리 확인 가능하다.

2. 호출자가 함수가 전지는 예외나 오류값에 대해서 몰라도 된다.

3. 부분 함수의 조합으로 부분 함수 자체를 재사용할 수 있고, 확장할 수도 있다.

 

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

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

 

 

 

부분적용함수

부분적용함수는 부분함수랑 관계가 없다. 일반적으로 함수를 만들대는 필요한 매개변수를 모두 전달받고 함수의 구현부에서 받은 매개변수를 사용하여 동작을 구현한다. 함수형 프로그래밍에서는 매개변수의 일부만 전달할 수도 있고 아예 전달하지 않을 수도 있다. 이렇게 매개변수의 일부만 전달받았을 때, 제공받는 매개변수만 가지고 부분 적용함수를 생성한다.

 

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

매개변수 두개를 받아서 값을 반환하는 함수의 확장함수 두개를 만들었다.

 

val func = { a: String, b: String -> a + b }
val partiallyAppliedFunc1 = func.partial1("Hello")
val result1 = partiallyAppliedFunc1("World")

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

부분 적용함수는 코드를 재사용하기위해도 쓸수있다.

 

 

커링함수

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

 

fun multiThree(a: Int, b: Int, c: Int): Int = a * b * c
    fun multiThree2(a: Int) = { b: Int -> { c: Int -> a * b * c } }
    fun curringTest(){
        val partial1 = multiThree2(1)
        val partial2 = partial1(2)
        val partial3 = partial1(3)
        multiThree2(1)(2)(3)
    }

이렇게 매개변수 3개를 받아서 곱하는 함수를 하나씩 해서 3개로 쪼갤 수 있다.

 

커링의 장점은 이런 부분적용함수를 다양하게 재사용할 수 있다는점이다. 또한 마지막 매개변수가 입력될 때까지

함수의 실행을 늦출수 있다.

 

지린다..

 

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

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

 

두개의 함수를 통해 커리드된것과 언커리드 된걸로 바꿀수 잇다.

 

 

합성함수

함수를 매개변수로 받고 함수를 반환할 수 있는 고차 함수를 이용해서 두 개의 함수를 결합하는 것을 말한다.

 

(f o x)(x) = f(g(x)) 와 같은것

 

순수함수형언어 하스겔은 합성함수를 포함하기 위한 '.' 연산을 통해 두개를 합성할수있다.

 

    fun comp() {
        val addThree = { i: Int -> i + 3 }
        val twice = { i: Int -> i * 2 }
        val composeFunc = addThree compose twice
    }

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

 

실제는 뒤에서부터 실행됨. twice 그다음 addThree

 

 

포인트 프리 스타일 프로그래밍

함수 합성을 사용해서 매개변수나 타입 선언없이 함수를 만드는 방식을 포인트 프리스타일 프로그래밍이라고 한다.

 

 

하나 이상의 매개변수를 받는 합성

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

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

 

 

fun <P1, P2, R> ((P1, P2) -> R).curried(): (P1) -> (P2) -> R =
            { p1: P1 -> { p2: P2 -> this(p1, p2) } }
        
val curriedGCD1 = ::gcd.curried()

함성함수와 매서드 레퍼런스를 통해 계산

매서드 래퍼런스는 말그대로 메서드에 대한 참조를 나타내기 때문에 (Int,Int) -> Int 이다.

 

여러 개의 매개변수에 동일한 함수를 적용해야 할 때, 함수 함성을 사용하는 것은 적합하지 않다. 이경우 고차함수로 연결하는것이 좋다.

 

 

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

 

    fun gocha() {
        val result = callback("1")("2")("3")("4")("5")
    }

    val callback: (String) -> (String) -> (String) -> (String) -> (String) -> String = { v1 ->
        { v2 ->
            { v3 ->
                { v4 ->
                    { v5 ->
                        v1 + v2 + v3 + v4 + v5
                    }
                }
            }
        }
    }

콜백리스너를 이렇게 가독성이 좋도록 대체 가능하다.