안녕하세요.
오늘은 함수형 프로그래밍에 필요한 기본 개념인 고차 함수에 대해 알아보도록 하겠습니다.
- 고차 함수 (High-Order Function)
- 부분 함수 (Partial Function)
- 커링 함수 (Currying Function)
- 합성 함수 (Composition Function)
일급 객체
함수형 프로그래밍에서는 함수를 객체처럼 다룹니다.
코틀린도 이러한 함수형 프로그래밍 기능을 지원하는 언어 중 하나입니다.
1) 고차 함수 (High-Order Function)
함수를 변수처럼 전달하고 반환할 수 있는 개념으로, 아래 2가지 기능이 가능합니다
- 함수를 매개변수로 받는 함수 ( highOrderFunction1 )
- 함수를 반환하는 함수 ( highOrderFunction2 )
fun highOrderFunction1(func : () -> Unit) {
func()
}
fun highOrderFunction2() : () -> Unit {
return { println("Hello World") }
}
장점
고차 함수를 사용함으로써 아래 이점을 얻을 수 있습니다.
1) 재사용성
Before - 상속을 사용한 예제
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
}
}
fun main() {
val calcSum = Sum()
val calcMinus = Minus()
val calcProduct = Product()
println(calcSum.calc(1, 5)) // 6
println(calcMinus.calc(5, 2)) // 3
println(calcProduct.calc(4, 2)) // 8
}
After
Override fun calc와 같은 boilerplate 코드가 줄어듭니다
fun highOrder(func: (Int, Int) -> Int, x: Int, y: Int): Int = func(x, y)
fun main() {
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(highOrder(sum, 1, 5)) // 6
println(highOrder(minus, 5, 2)) // 3
println(highOrder(product, 4, 2))// 8
}
2) 간결성
Before - 명령형 프로그래밍
fun main() {
val ints = 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]
}
After
fun main() {
val ints = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val result = ints
.map { it * 2 }
.filter { it > 10 }
println(result) // [12, 14, 16, 18, 20]
}
2) 부분 함수 (Partial Function)
일부 입력에 대해서만 정의된 함수입니다.
→ 입력 값이 특정 조건을 만족할 때만 결과를 반환 / 그렇지 않으면 실행되지 않는 함수
// 일반 함수 (모든 입력에 대해 정의됨)
fun square(x: Int): Int = x * x
// 부분 함수 (양수에 대해서만 정의됨)
fun positiveSquare(x: Int): Int? {
return if (x >= 0) x * x else null
}
fun main() {
println(square(4)) // 16
println(positiveSquare(4)) // 16
println(positiveSquare(-3)) // null (정의되지 않음)
}
활용
condition → 함수가 특정 값에 정의되어 있는지 확인하는 함수
applyIfDefined → 정의된 경우에만 실행하는 함수
class PartialFunction<in P, out R>(
private val condition: (P) -> Boolean,
private val apply: (P) -> R
) {
fun isDefined(value: P): Boolean = condition(value)
fun applyIfDefined(value: P): R? = when {
condition(value) -> apply(value)
else -> null
}
}
fun main() {
val positiveSquare = PartialFunction<Int, Int>(
condition = { it >= 0 },
apply = { it * it }
)
println(positiveSquare.applyIfDefined(4)) // 16
println(positiveSquare.applyIfDefined(-3)) // null
}
필요성
가장 좋은 방법은 부분 함수를 만들어야 하는 상황을 만들지 않는 것입니다.
- 함수형 프로그래밍에서 함수를 만들 때는 가급적 모든 입력에 대한 결과를 정의하는 것이 좋습니다
물론 불가피하게 작성해야 하는 경우도 있습니다.
- 리스트의 첫번째나 마지막 값을 꺼내는 함수에서 빈 리스트에 접근했을 때 처리하는 방법
- 이 같은 경우에 부분 함수를 활용합니다
3) 커링 함수 (Currying Function)
여러 개의 매개변수를 받는 함수를 분리하여, 단일 매개변수를 받는 부분 적용 함수의 체인으로 만드는 방법
- 다중 인자 함수 → 일련의 단일 인자 함수로 바꾸는 과정
- change f(a, b, c) to f(a) → f(b) → f(c)
+) 부분 적용 함수
여러 개의 매개변수를 받는 함수에서 일부 인자만 미리 적용하여 새로운 함수를 만드는 방법
fun multiply(a: Int, b: Int) = a * b
fun main() {
// 'a' 값을 2로 고정한 부분 적용 함수
val double = { x: Int -> multiply(2, x) }
println(double(5)) // 10
println(double(10)) // 20
}
Currying 함수 예제
operation(5) → b를 받는 함수 반환
operation(5)(3) → op를 받는 함수 반환
operation(5)(3)(Int::plus) → 최종 계산 수행
fun operation(a: Int) = { b: Int -> { op: (Int, Int) -> Int -> op(a, b) } }
fun main() {
val addWith5 = operation(5)(3)(Int::plus) // 5 + 3 = 8
val multiplyWith4 = operation(4)(2)(Int::times) // 4 * 2 = 8
println(addWith5) // 8
println(multiplyWith4) // 8
}
4) 합성 함수 (Composition Function)
두 개 이상의 함수를 결합하여 새로운 함수를 만드는 기법
(f ∘ g)(x) = f(g(x))
예제)
1- 일반적인 예제
fun double(x: Int) = x * 2
fun square(x: Int) = x * x
fun main() {
val composedFunction = { x: Int -> square(double(x)) }
println(composedFunction(3)) // (3 * 2) ^ 2 = 36
}
2- 확장 함수 예제
infix fun <A, B, C> ((B) -> C).compose(other: (A) -> B): (A) -> C {
return { x: A -> this(other(x)) }
}
fun main() {
val double = { x: Int -> x * 2 }
val square = { x: Int -> x * x }
val composed = square compose double // f ∘ g
println(composed(3)) // (3 * 2) ^ 2 = 36
}
포인트 프리 스타일
함수를 정의할 때 입력 값을 직접 명시하지 않고, 함수 조합만으로 표현하는 방식을 의미합니다.
즉, 함수의 인자를 생략하고 함수의 조합만으로 원하는 로직을 구현하는 방식입니다.
infix fun <A, B, C> ((B) -> C).compose(other: (A) -> B): (A) -> C {
return { x: A -> this(other(x)) }
}
fun main() {
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, 5, -2, -4, 8, 14))
println(result) // -14
}
참고
코틀린으로 배우는 함수형 프로그래밍 4장 고차함수
https://www.yes24.com/product/goods/84899008
코틀린으로 배우는 함수형 프로그래밍 - 예스24
차세대 언어 코틀린을 이용해실전에서 활용할 수 있는 함수형 코드를 설계한다!어렵게만 느껴지는 함수형 개념을 충실히 설명하여, 실전에서 활용할 수 있는 지식이 될 수 있도록 한 책이다. 다
www.yes24.com