코틀린 (Kotlin) 고차함수

코틀린 고차함수

  • 재사용성
    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 }
   
    println(higherOrder(sum, 1, 5))     // 6
    println(higherOrder(minus, 5, 2))   // 3
    println(higherOrder(product, 4, 2)) // 8
  • 기능확장성
    val twiceSum: (Int, Int) -> Int = { x, y -> (x + y) * 2 }
    println(higherOrder(twiceSum, 8, 2))   // 20
  • 간결성
    • map, filter 로 간결하고 가독성을 향상시킴
    val result2 = ints
            .map { it * 2 }
            .filter { it > 10 }

    println(result2)
    

부분 함수

  • 특정한 값이나 범위내에 있을 때만 함수가 동작하도록 할수 있음
  • 호출자가 함수가 던지는 예외나 오류값에 대해서 몰라도 된다
  • 부분 함수의 조합으로 부분 함수 자체를 재사용할 수도있고, 확장할 수도 있다
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)
}

    val condition: (Int) -> Boolean = { it in 1..3 }
    val body: (Int) -> String = {
        when (it) {
            1 -> "One!"
            2 -> "Two!"
            3 -> "Three!"
            else -> "Not between 1 and 3"
        }
    }

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

위에 코드를 확장함수로 변환

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

fun testToPartialFunction() {
    val condition: (Int) -> Boolean = { 0 == it.rem(2) }
    val body: (Int) -> String = { "$it is even" }

    val isEven = body.toPartialFunction(condition)

    if (isEven.isDefinedAt(100)) {
        println(isEven(100))     // "100 is even!"
    } else {
        println("isDefinedAt(x) return false")
    }
}

부분 적용 함수

  • 매개변수의 일부만 전달받았을 때 부분 적용 함수 생성
  • 코드 재사용성과 커링 함수(curried functions) 에 필요
fun main() {
    val pf1 = {a: String, b: String -> a+b}.partial("a") //일부만 전달받아 함수 참조 생성
    println(pf1("b"))
}

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

커링 함수

  • 매개변수를 받는 함수를 분리하여, 단일 매개변수를 받는 부분 적용 함수의 체인으로 반드는 방법
  • 부분 적용 함수를 재사용할 수 있는 것
  • 마지막 매개변수가 입력될때까지 함수 실행을 늦출 수 있음
fun multiThree(a: Int, b: Int, c: Int) : Int = a*b*c
위 코드를 커링함수로 변환한다

fun multiThree(a: Int) = { b: Int -> { c:Int -> a*b*c}} //커링함수 

val p1 = multiThree(1) //부분 적용 함수
val p2 = p1(2) //부분 적용 함수
val p3 = p2(3) //부분 적용 함수
println(p3) //실행
println(multiThree(1)(2)(3)) //함수를 커링으로 쪼갰기때문에 (1)(2)(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)}

val m = {a: Int, b: Int, c:Int -> a*b*c}
val curried = m.curried()
println(curried(1)(2)(3))

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

합성 함수

  • 함수를 매개변수로 받고 함수를 반환할 수 있는 고차 함수를 이용해서 두 개의 함수를 결합하는 것
fun twice(i: Int) = i * 2
fun addThree(i:Int) = i + 3
fun composed(i:Int) = addThree(twice(i))

println(composed(3))

합성 함수 일반화 하기

  • (F)->R 의 반환값 타입이 g함수의 매개변수 타입이 같으면 함수 합성이 가능
//infix 는 입력 매개변수를 양쪽에서 받을 수 있음
infix fun <F,G,R> ((F) -> R).compose(g:(G) -> F): (G) -> R {
    return { gInput:G -> this(g(gInput))}
}

val addThree = {i: Int -> i + 3}
val twice = {i: Int -> i * 2}
//실행은 twice 부터 실행되고 반환값이 addthree로 입력
val composedFunc = addThree compose twice
println(composedFunc(3))

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

  • 함수 합성을 사용해서 매개변수나 타입 선언 없이 함수를 만드는 것
  • 코드 가독성을 높이고 간결
  • 단순한 함수를 만들고 조합하여 복잡한 함수를 만들수도 있음
  • 지나친 체인 함수는 가독성을 해칠수 있기때문에 적절하게 사용

val absolute = { i: List<Int> -> i.map{ it -> Math.abs(it)}}
val negative = { i: List<Int> -> i.map{ it -> -it }}
val minimum = { i: List<Int> -> i.min() }
minimum(negative(absolute(listOf(3,-1,4))))

위 코드를 포인트 프리 스타일로 변경 ->
val compose = minimum compose negative compose absolute
val result = compose(listOf(3,-1,4))
println(result)

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

  • compose 는 매개변수가 하나이기때문에 커링을 사용해서 매개변수 한개로 체인이 되도록 함
val powerOfTwo = { x: Int -> power(x.toDouble(), 2).toInt()}
val curriedGcd1 = ::gcd.curried()
val composedGcdPowerOfTwo1 = curriedGcd1 compose powerOfTwo
println(composedGcdPowerOfTwo1(25)(5))

위 코드는 잘못된 값이 출력된다.
매개 변수가 두개인 함수를 커링했기때문에 powerOfTwo 결과값이 composedGcdPowerOfTwo1 의 첫번째 매개변수에만 할당되고 두 번째 매개변수에는 전달되지 않았다.

val curriedGcd2 = {m:Int, n:Int -> gcd(m, powerOfTwo(n))}.curried()
val composedGcdPowerOfTwo2 = curriedGcd2 compose powerOfTwo
println(composedGcdPowerOfTwo2(25)(5))

위 코드처럼 수정하면 curriedGcd2 의 두번째 매개변수도 powerOfTwo() 가 되도록 한다.

  • 좋은 코드가 아니므로 매개변수가 여러개이고 동일한 함수를 적용해야되는 경우 합성 함수말고 아래 코드처럼 하는것이 좋다.
 val powerOfTwo = { x: Int -> power(x.toDouble(), 2).toInt()}
 val gcdt = { x1: Int, x2:Int -> gcd(powerOfTwo(x1), powerOfTwo(x2))}
 println(gcdt(25,5))

고차 함수의 사용

  • 코드를 작성할 때 자주 사용되는 패턴을 추상화하기 위해 고차 함수를 사용한다
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]
}

fun <T> Collection<T>.head() = first()
fun <T> Collection<T>.tail() = drop(1)
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)
    }
}

콜백 리스너 대체

  • 고차함수와 커링사용시 가독성 개선
  • 체이닝으로 단계마다 평가되는것이 아니라  필요한 시점에서 게으르게 평가 됨
val callback: (String) -> (String) -> (String) -> (String) -> (String) -> String = { v1 ->
    { v2 ->
        { v3 ->
            { v4 ->
                { v5 ->
                    v1 + v2 + v3 + v4 + v5
                }
            }
        }
    }
}

val result = callback("1")("2")("3")("4")("5")
println(result) //12345

val result1 = callback("1")("2")
println(result1("3")("4")("5")) //12345
  • 커링을 이용해서 부분 적용 함수로 재사용성을 높임
val partialApp = callback("prefix")(":") //공통영역
println(partialApp("1")("2")("3"))
println(partialApp("a")("b")("b"))





*개인적으로 코틀린을 공부하면서 정리한 자료입니다. 수정 사항 및 이슈가 있는 경우 메일 부탁드립니다.

댓글

이 블로그의 인기 게시물

코틀린 (Kotlin) filter, map, all, any, count, find, groupBy, flatMap 함수 정리

코틀린 (Kotlin) 인터페이스 정리

RecyclerView 에서 notifyItemChanged()의 payload 이해하기