커링(currying)이란 여러 개의 매개변수를 받는 함수를 분리하여, 단일 매개변수를 받는 부분 적용 함수의 체인으로 만드는 방법이다. 커링의 장점은 부분 적용 함수를 다양하게 재사용할 수 있다는 점이며, 마지막 매개변수가 입력될 때까지 함수의 실행을 늦출 수 있다. 코틀린 확장 함수를 사용하여 매개변수가 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) }
fun main(args: Array<String>) {
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" 출력
}
합성 함수란 함수를 매개변수로 받고, 함수를 반환할 수 있는 고차 함수를 이용해서 두 개의 함수를 결합하는 것을 말한다. 합성 함수를 간결하게 생성하기 위한 확장 함수를 구현해보자.
fun main(args: Array<String>) {
val addThree = { i: Int -> i + 3 }
val twice = { i: Int -> i * 2 }
val composedFunc = addThree compose twice
println(composedFunc(3)) // "9" 출력
}
infix fun <F, G, R> ((F) -> R).compose(g: (G) -> F): (G) -> R {
return { gInput: G -> this(g(gInput)) }
}
compose 함수를 사용하지 않고, 함수를 매개변수로 넣어서 호출하는 예를 살펴보자. 입력 숫자들의 리스트를 모두 음수로 만든 후, 최솟값을 구하는 함수이다.
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, 4, 2, 9, 20))
println(result) // "-20" 출력
이처럼 함수 합성을 사용해서 매개변수나 타입 선언 없이 함수를 만드는 방식을 포인트 프리 스타일(point free style) 프로그래밍이라 한다. 포인트 프리 스타일은 가독성을 높이고 간결하게 하는 장점이 있지만, 지나치게 많은 함수를 체인으로 만드는 것이 가독성을 해칠 수도 있으므로 조심해야 한다.
한 개의 함수와 두 개의 리스트를 입력으로 받은 후, 두 개의 리스트 값을 입력받은 함수에 적용하고 합쳐진 리스트를 반환하는 함수이다.
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(args: Array<String>) {
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]" 출력
}
위와 같은 방법으로 실제 프로그래밍을 할 때에도 함수형 프로그래밍을 적용할 수 있다. 자주 사용되는 패턴을 추상화하기위해 고차 함수를 사용하여 코드의 재사용성을 높일 수 있다.