함수형 프로그래밍
안녕하세요. 빅토리아입니다. 오늘은 [코틀린으로 함수형 프로그래밍 시작하기] 책의 4장 고차 함수에 대해 알아보겠습니다.
고차 함수란?
코틀린에서의 함수는 일급함수이기 때문에, 함수를 인자로 받거나 반환값으로 반환하는 것이 가능합니다.
함수를 매개변수로 받는 함수
함수를 반환하는 함수
위의 두 가지 조건 중 하나 이상을 만족하는 함수를 고차 함수라고 합니다.
fun higherOrderFunction1(func: () -> Unit): Unit{ func() }
fun higherOrderFunction2(): () -> Unit{ return {println('Hello, This is higherOrderFunction')} }
higherOrderFunction1의 경우 매개변수로 함수를 전달받고, higherOrderFunction2의 경우 함수를 반환하기 때문에 모두 고차 함수입니다.
고차 함수를 사용하게 되면 코드의 재사용성, 기능 확장성, 간결성을 향상시킬 수 있습니다.
재사용성
상속을 사용하여 구현한 계산기와 고차함수를 사용해서 구현한 계산기의 코드를 비교해봅시다.
fun main(args:Array){
val calcSum = Sum()
val calcMinus = Minus()
println(calcSum.calc(1,5)) //output : 6
println(calcMinus.calc(5,2)) //output : 3
}
interface Calcable(){
fun calc(x:Int, y:Int): Int
}
class Sum: Calcable {
override fun clac(x:Int, y:Int): Int{
return x+y
}
}
class Minus: Calcable {
override fun clac(x:Int, y:Int): Int{
return x-y
}
}
상속을 활용하여 구현한 계산기에서 핵심적인 비즈니스 로직은 x+y, x-y라고 할 수 있습니다. 위 코드는 override fun clac(x:Int, y:Int): Int 와 같은 상용구 코드(boilerplate code)들이 반복해서 나타나 핵심 로직이 쉽게 눈에 보이지 않으며, 요구사항이 추가될 때마다 상용구 코드들이 점점 늘어나게 됩니다.
반면 고차함수를 활용하여 구현하면 좀 더 간결하고 재사용성이 높은 코드로 만들 수 있습니다.
fun main(args:Array){
val sum: (Int, Int) -> Int = {x,y -> x+y} val minus: (Int, Int) -> Int = {x,y -> x-y}
println(higherOrder(sum, 1, 5)) //output : 6
println(higherOrder(minus, 5, 2)) //output : 3
} fun higherOrder(func: (Int, Int)-> Int, x:Int, y:Int): Int = func(x,y)
higherOrder라는 함수는 func이라는 함수를 매개변수로 받고 있으므로 고차 함수입니다. 매개변수로 받는 함수는 오직 타입으로 일반화 되어 있고, 비즈니스 로직은 호출자로부터 주입을 받습니다.
기능 확장성
상속을 활용한 계산기 코드에 곱셈 기능을 추가하려면,
fun main(args:Array){
val calcProduct = Product()
println(calcProduct.calc(5,2)) //output : 10
} class Product: Calcable {
override fun clac(x:Int, y:Int): Int{
return x*y
}
}
override fun clac(x:Int, y:Int): Int 와 같은 상용구 코드(boilerplate code)를 추가하여 작성해야 한다는 단점이 있습니다.
반면, 고차 함수를 사용한 계산기 코드의 경우,
fun main(args:Array){
val product: (Int, Int) -> Int = {x,y -> x*y}
println(higherOrder(product, 5, 2)) //output : 10
}
x*y 라는 핵심 비즈니스 로직만 추가하면 기능을 확장할 수 있습니다.
간결성
입력 리스트의 값들을중 짝수의 값들을 리스트로 반환하는 예제를 명령형 프로그래밍으로 작성한 코드와 함수형 프로그래밍으로 작성한 두 코드를 비교해봅시다.
val evenNumbers: ArrayList= ArrayList()
for(i in 0 until ints.size) {
If(ints[i] % 2 == 0) {
evenNumbers.add(ints[i])
}
}
동일한 코드를 코틀린의 컬렉션 API와 고차 함수를 사용해서 작성해보면,
val ints: List= listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val result = ints .filter {value -> value%2==0}
filter를 사용하여 코드가 간결하고 가독성이 좋습니다. map이나 filter 함수는 함수를 인자로 받는 고차함수 이기 때문에 위와 같이 간결하게 코드를 작성할 수 있습니다 😃
부분 함수
부분 함수란 모든 가능한 입력들 중, 일부 입력에 대한 결과만 정의한 함수를 의미합니다.
어떤 함수의 입력이 특정 값이나 범위내에 있을 때만 해당 함수를 정상적으로 동작시키고 싶을 때, 부분 함수를 사용하여 해결할 수 있습니다.
fun twice(x:Int) = x * 2
fun partialTwice(x:Int): Int =
if (x<100){
x*2
} else {
throw IllegalArgumentException()
}
twice 함수의 경우, 모든 입력값을 두 배를 하지만, partialTwice 함수는 입력값이 100보다 작은 경우에만 두 배를 한다. partialTwice 함수의 x값은 twice의 x값의 부분집합이므로, partialTwice는 twice의 부분 함수입니다. partialTwice의 경우, 입력값이 100보다 작은 경우엔 예외를 발생시킵니다.
부분 함수 만들기
class PartialFunction(
private val condition: (P) -> boolean,
private cal f: (P) -> R
) : (P) -> R {
override fun invoke(p: P): R = when {
condition(p) -> f(p)
else -> throw IllegalArgumentException('$p isn't supported')
}
func isDefinedAt(p: P) : Boolean = condition(p)
}
PartialFunction의 생성자는 입력값을 확인하는 함수 condition과 조건을 만족했을 때, 수행할 함수 f를 매개변수로 받습니다. invoke 함수의 입력값 p가 condition 함수에 정의된 조건에 맞을 때만 f 함수가 실행되고 조건에 맞지 않을 때 예외를 발생시킵니다. 추가적으로, p가 입력 조건에 맞는지 사전에 확인할 수 있는 isDefinedAt 함수가 존재합니다.
val isEven = PartialFunction({it%2 == 0}, {'$it is even'}) if(isEven.isDefinedAt(100)) { // isDefined 함수로 입력 조건을 사전에 체크가능 print(isEven(100)) } else { print('isDefinedAt(x) return false') }
위 코드는 PartialFunction에 {it%2 == 0}, {“$it is even”} 의 두 람다 함수를 매개변수로 넘겼습니다. isDefinedAt 함수를 이용하여 사전에 예외 발생을 방지하고, 부분 입력에 대한 처리를 할 수 있습니다.
부분 함수의 필요성
부분 함수를 사용하면 위 예시와 같이 isDefinedAt 함수를 이용하여 미리 입력 조건을 확인하여 함수가 예외를 던지거나 오류값을 반환하는 것을 방지할 수 있습니다.
부분함수를 사용하면 다음과 같은 장점을 가집니다.
호출하는 쪽에서 호출하기 전에 함수가 정상적으로 동작하는지의 여부를 미리 확인할 수 있습니다.
호출자가 함수가 던지는 예외나 오류값에 대해서 몰라도 됩니다.
부분 함수의 조합으로 부분 함수 자체를 재사용할 수도 있고, 확장할 수 있습니다.
부분 적용 함수
일반적으로 함수를 생성할 때는 필요한 매개변수를 모두 전달받고, 해당 매개변수를 사용하여 동작을 구현하지만, 함수형 프로그래밍에서는 매개변수의 일부만 전달할 수도 있고, 아예 전달하지 않을 수도 있습니다.
이렇게 매개변수의 일부만 전달받았을 때, 제공받은 매개변수만 가지고 부분 적용 함수를 생성할 수 있습니다.
부분 적용함수를 생성하기 위해서 간단한 확장함수를 만들어봅시다.
fun((P1, P2)-> R).partial(p1: P1): (P2) -> R { return {p2 -> this(p1,p2)} }
위 함수는 매개변수 두 개를 받아서 값을 반환하는 함수 ((p1,p2 ) -> R)의 확장 함수 partial을 만들었습니다. partial(p1: P1)함수는 첫 번째 매개변수 p1을 받아 적용하고 (P2) -> R 함수를 반환합니다.
fun main(args:Array){
val func = {a: String, b: String -> a+b }
val partialAppliedFunc = func.partial('Hello')
val result = partialAppliedFunc('World')
println(result) // output : 'HelloWorld'
}
partialAppliedFunc 함수는 “Hello”라는 매개변수만 적용된 부분 적용함수이다. 여기에 두번째 매개변수인 “World”를 넣어서 호출하면 출력 결과는 “HelloWorld”가 됩니다.
이러한 부분 적용 함수는 코드를 재사용하기 위해서 쓸 수도 있지만, 다음 장에 배울 커링함수를 만들기 위해서 필요한 개념입니다.
지금까지 고차함수와 부분함수, 부분 적용함수에 대해 살펴보았습니다. 읽어주셔서 감사합니다 🙂