함수형 프로그래밍 in 코틀린 - 고차 함수[4]
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
}