함수형 프로그래밍에서는 다음 두 가지 조건 중 하나 이상을 만족하는 함수를 고차 함수(higher-order function)라고 한다.
고차 함수를 사용하면 코드의 재사용성을 높일 수 있고, 기능을 확장하기 쉬우며, 코드를 간결하게 작성할 수 있다. 고차 함수를 사용하여 재사용성을 높인 계산기 코드를 살펴보자.
fun main(args: Array<String>) {
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(higherOrder(sum, 1, 5)) // "6" 출력
println(higherOrder(minus, 5, 2)) // "3" 출력
println(higherOrder(product, 4, 2)) // "8" 출력
}
fun higherOrder(func: (Int, Int) -> Int, x: Int, y: Int): Int = func(x, y)
higherOrder는 함수를 매개변수로 받고 있으므로 고차 함수이다. 매개변수로 받은 함수는 오직 타입으로만 일반화되어 있다. 비즈니스 로직은 호출자로부터 주입받는다. 이 경우에 기능의 확장 또한 쉬워진다. 다른 기능을 가지고 있는 함수를 매개변수로 넘겨주면 쉽게 구현할 수 있다.
어떤 함수의 입력이, 특정한 값이나 범위 내에 있을 때만 함수를 정상적으로 동작시키려면, 함수형 언어에서는 부분함수를 사용하여 문제를 해결할 수 있다. 부분 함수(partial function)란 모든 가능한 입력 중, 일부 입력에 대한 결과만을 정의한 함수를 의미한다. Kotlin에서는 언어 차원에서 제공하는 부분 함수 클래스가 존재하지 않는다. 실제로 프로그래밍에 활용할 수 있는 부분 함수를 만들어 보자.
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)
}
PartialFunction의 생성자는 입력값을 확인하는 함수 condition과, 조건에 만족했을 때 수행할 함수 f를 매개변수로 받는다. invoke 함수의 입력값 p가 condition 함수에 정의된 조건에 맞을 때만 f 함수가 실행되고, 조건에 맞지 않으면 예외를 발생시킨다. 추가로 입력값 p가 입력 조건에 맞는지 사전에 확인할 수 있는 isDefinedAt 함수를 제공한다. PartialFunction을 활용하여 주어진 값이 짝수인지 여부를 확인하는 부분 함수를 구현하면
val isEven = PartialFunction<Int, String>({ it % 2 == 0 }, { "$it is even" })
if (isEven.isDefinedAt(100)) {
print(isEven(100)) // "100 is even" 출력
} else {
print("isDefinedAt(x) return false")
}
이 경우, 컴파일러가 부분 함수의 입력(P)를 추론할 수 없기 때문에 PartialFunction에 타입 <Int, String>을 명시했다. 이 부분 함수를 좀 더 간결하게 생성하기 위해서 toPartialFunction 확장 함수를 만들어 보자.
fun <P, R> ((P) -> R).toPartialFunction(definedAt: (P) -> Boolean)
: PartialFunction<P, R> = PartialFunction(definedAt, this)
val condition: (Int) -> Boolean = { it.rem(2) == 0 }
val body: (Int) -> String = { "$it is even" }
val isEven = body.toPartialFunction(condition)
if (isEven.isDefinedAt(100)) {
print(isEven(100)) // "100 is even" 출력
} else {
print("isDefinedAt(x) return false")
}
부분 함수와 이름이 비슷하지만 관계는 없다. 일반적으로 함수를 만들 때는 필요한 매개변수를 모두 전달받고, 함수의 구현부에서 받은 매개변수를 사용하여 동작을 구현한다. 함수형 프로그래밍에서는 매개변수의 일부만 전달할 수도 있고 아예 전달하지 않을 수도 있다. 이렇게 매개변수의 일부만 전달받았을 때, 제공받은 매개변수만 가지고 부분 적용 함수를 생성한다. 코틀린은 부분 적용 함수를 기본 함수로 제공하지 않으므로, 확장함수를 만들어 보자.
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) }
}
partial1 함수는 첫 번째 매개변수 p1만 받아서 적용하고 (P2) -> R
함수를 반환한다. 이때 (P2) -> R
함수는 첫 번째 매개변수만 적용된 부분 적용 함수이다. partial2는 p2만 받아서 적용하고 (P1) -> R
함수를 반환한다. 이제 위의 두 함수를 사용하여 부분 적용 함수를 생성해보자.
fun main(args: Array<String>( {
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" 출력
}
partiallyAppliedFunc1은 값으로 평가되지 않고, 남은 매개변수를 받아서 결과를 반환하는 함수의 참조만 가지고 있다. 위의 예시에서는 입력 매개변수가 두 개인 확장 함수만 만들었지만, 매개변수를 많이 받는 함수에 대한 확장 함수도 만들 수 있다.
💡 "함수에 어떤 값을 적용(applied)했다"는 표현은 "어떤 값을 함수의 매개변수로 넣는다"