코틀린 (Kotlin) 고차함수
코틀린 고차함수
- 재사용성
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 }
println(higherOrder(sum, 1, 5)) // 6
println(higherOrder(minus, 5, 2)) // 3
println(higherOrder(product, 4, 2)) // 8
- 기능확장성
val twiceSum: (Int, Int) -> Int = { x, y -> (x + y) * 2 }
println(higherOrder(twiceSum, 8, 2)) // 20
- 간결성
- map, filter 로 간결하고 가독성을 향상시킴
val result2 = ints
.map { it * 2 }
.filter { it > 10 }
println(result2)
부분 함수
- 특정한 값이나 범위내에 있을 때만 함수가 동작하도록 할수 있음
- 호출자가 함수가 던지는 예외나 오류값에 대해서 몰라도 된다
- 부분 함수의 조합으로 부분 함수 자체를 재사용할 수도있고, 확장할 수도 있다
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)
}
val condition: (Int) -> Boolean = { it in 1..3 }
val body: (Int) -> String = {
when (it) {
1 -> "One!"
2 -> "Two!"
3 -> "Three!"
else -> "Not between 1 and 3"
}
}
val oneTwoThree = PartialFunction(condition, body)
if (oneTwoThree.isDefinedAt(3)) {
println(oneTwoThree(3))
} else {
println("isDefinedAt(x) return false")
}
위에 코드를 확장함수로 변환
fun <P, R> ((P) -> R).toPartialFunction(definedAt: (P) -> Boolean)
: PartialFunction<P, R> = PartialFunction(definedAt, this)
fun testToPartialFunction() {
val condition: (Int) -> Boolean = { 0 == it.rem(2) }
val body: (Int) -> String = { "$it is even" }
val isEven = body.toPartialFunction(condition)
if (isEven.isDefinedAt(100)) {
println(isEven(100)) // "100 is even!"
} else {
println("isDefinedAt(x) return false")
}
}
val p1 = multiThree(1) //부분 적용 함수
val p2 = p1(2) //부분 적용 함수
val p3 = p2(3) //부분 적용 함수
println(p3) //실행
println(multiThree(1)(2)(3)) //함수를 커링으로 쪼갰기때문에 (1)(2)(3) 으로 호출 가능
{ 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)}
val m = {a: Int, b: Int, c:Int -> a*b*c}
val curried = m.curried()
println(curried(1)(2)(3))
val uncurried = curried.uncurried()
println(uncurried(1,2,3))
fun addThree(i:Int) = i + 3
fun composed(i:Int) = addThree(twice(i))
println(composed(3))
infix fun <F,G,R> ((F) -> R).compose(g:(G) -> F): (G) -> R {
return { gInput:G -> this(g(gInput))}
}
val addThree = {i: Int -> i + 3}
val twice = {i: Int -> i * 2}
//실행은 twice 부터 실행되고 반환값이 addthree로 입력
val composedFunc = addThree compose twice
println(composedFunc(3))
val absolute = { i: List<Int> -> i.map{ it -> Math.abs(it)}}
val negative = { i: List<Int> -> i.map{ it -> -it }}
val minimum = { i: List<Int> -> i.min() }
minimum(negative(absolute(listOf(3,-1,4))))
위 코드를 포인트 프리 스타일로 변경 ->
val compose = minimum compose negative compose absolute
val result = compose(listOf(3,-1,4))
println(result)
val curriedGcd1 = ::gcd.curried()
val composedGcdPowerOfTwo1 = curriedGcd1 compose powerOfTwo
println(composedGcdPowerOfTwo1(25)(5))
위 코드는 잘못된 값이 출력된다.
매개 변수가 두개인 함수를 커링했기때문에 powerOfTwo 결과값이 composedGcdPowerOfTwo1 의 첫번째 매개변수에만 할당되고 두 번째 매개변수에는 전달되지 않았다.
val curriedGcd2 = {m:Int, n:Int -> gcd(m, powerOfTwo(n))}.curried()
val composedGcdPowerOfTwo2 = curriedGcd2 compose powerOfTwo
println(composedGcdPowerOfTwo2(25)(5))
위 코드처럼 수정하면 curriedGcd2 의 두번째 매개변수도 powerOfTwo() 가 되도록 한다.
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]
}
fun <T> Collection<T>.head() = first()
fun <T> Collection<T>.tail() = drop(1)
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)
}
}
{ v2 ->
{ v3 ->
{ v4 ->
{ v5 ->
v1 + v2 + v3 + v4 + v5
}
}
}
}
}
val result = callback("1")("2")("3")("4")("5")
println(result) //12345
val result1 = callback("1")("2")
println(result1("3")("4")("5")) //12345
println(partialApp("1")("2")("3"))
println(partialApp("a")("b")("b"))
*개인적으로 코틀린을 공부하면서 정리한 자료입니다. 수정 사항 및 이슈가 있는 경우 메일 부탁드립니다.
val body: (Int) -> String = {
when (it) {
1 -> "One!"
2 -> "Two!"
3 -> "Three!"
else -> "Not between 1 and 3"
}
}
val oneTwoThree = PartialFunction(condition, body)
if (oneTwoThree.isDefinedAt(3)) {
println(oneTwoThree(3))
} else {
println("isDefinedAt(x) return false")
}
위에 코드를 확장함수로 변환
fun <P, R> ((P) -> R).toPartialFunction(definedAt: (P) -> Boolean)
: PartialFunction<P, R> = PartialFunction(definedAt, this)
fun testToPartialFunction() {
val condition: (Int) -> Boolean = { 0 == it.rem(2) }
val body: (Int) -> String = { "$it is even" }
val isEven = body.toPartialFunction(condition)
if (isEven.isDefinedAt(100)) {
println(isEven(100)) // "100 is even!"
} else {
println("isDefinedAt(x) return false")
}
}
부분 적용 함수
- 매개변수의 일부만 전달받았을 때 부분 적용 함수 생성
- 코드 재사용성과 커링 함수(curried functions) 에 필요
fun main() {
val pf1 = {a: String, b: String -> a+b}.partial("a") //일부만 전달받아 함수 참조 생성
println(pf1("b"))
}
fun <P1,P2,R> ((P1, P2) -> R).partial(p1: P1): (P2)->R {
return { p2 -> this(p1, p2)}
}
커링 함수
- 매개변수를 받는 함수를 분리하여, 단일 매개변수를 받는 부분 적용 함수의 체인으로 반드는 방법
- 부분 적용 함수를 재사용할 수 있는 것
- 마지막 매개변수가 입력될때까지 함수 실행을 늦출 수 있음
fun multiThree(a: Int, b: Int, c: Int) : Int = a*b*c
위 코드를 커링함수로 변환한다
fun multiThree(a: Int) = { b: Int -> { c:Int -> a*b*c}} //커링함수
val p2 = p1(2) //부분 적용 함수
val p3 = p2(3) //부분 적용 함수
println(p3) //실행
println(multiThree(1)(2)(3)) //함수를 커링으로 쪼갰기때문에 (1)(2)(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)}
val m = {a: Int, b: Int, c:Int -> a*b*c}
val curried = m.curried()
println(curried(1)(2)(3))
val uncurried = curried.uncurried()
println(uncurried(1,2,3))
합성 함수
- 함수를 매개변수로 받고 함수를 반환할 수 있는 고차 함수를 이용해서 두 개의 함수를 결합하는 것
fun addThree(i:Int) = i + 3
fun composed(i:Int) = addThree(twice(i))
println(composed(3))
합성 함수 일반화 하기
- (F)->R 의 반환값 타입이 g함수의 매개변수 타입이 같으면 함수 합성이 가능
infix fun <F,G,R> ((F) -> R).compose(g:(G) -> F): (G) -> R {
return { gInput:G -> this(g(gInput))}
}
val addThree = {i: Int -> i + 3}
val twice = {i: Int -> i * 2}
//실행은 twice 부터 실행되고 반환값이 addthree로 입력
val composedFunc = addThree compose twice
println(composedFunc(3))
포인트 프리 스타일 프로그래밍
- 함수 합성을 사용해서 매개변수나 타입 선언 없이 함수를 만드는 것
- 코드 가독성을 높이고 간결
- 단순한 함수를 만들고 조합하여 복잡한 함수를 만들수도 있음
- 지나친 체인 함수는 가독성을 해칠수 있기때문에 적절하게 사용
val absolute = { i: List<Int> -> i.map{ it -> Math.abs(it)}}
val negative = { i: List<Int> -> i.map{ it -> -it }}
val minimum = { i: List<Int> -> i.min() }
minimum(negative(absolute(listOf(3,-1,4))))
위 코드를 포인트 프리 스타일로 변경 ->
val compose = minimum compose negative compose absolute
val result = compose(listOf(3,-1,4))
println(result)
하나 이상의 매개변수를 받는 함수의 합성
- compose 는 매개변수가 하나이기때문에 커링을 사용해서 매개변수 한개로 체인이 되도록 함
val curriedGcd1 = ::gcd.curried()
val composedGcdPowerOfTwo1 = curriedGcd1 compose powerOfTwo
println(composedGcdPowerOfTwo1(25)(5))
위 코드는 잘못된 값이 출력된다.
매개 변수가 두개인 함수를 커링했기때문에 powerOfTwo 결과값이 composedGcdPowerOfTwo1 의 첫번째 매개변수에만 할당되고 두 번째 매개변수에는 전달되지 않았다.
val curriedGcd2 = {m:Int, n:Int -> gcd(m, powerOfTwo(n))}.curried()
val composedGcdPowerOfTwo2 = curriedGcd2 compose powerOfTwo
println(composedGcdPowerOfTwo2(25)(5))
위 코드처럼 수정하면 curriedGcd2 의 두번째 매개변수도 powerOfTwo() 가 되도록 한다.
- 좋은 코드가 아니므로 매개변수가 여러개이고 동일한 함수를 적용해야되는 경우 합성 함수말고 아래 코드처럼 하는것이 좋다.
val powerOfTwo = { x: Int -> power(x.toDouble(), 2).toInt()}
val gcdt = { x1: Int, x2:Int -> gcd(powerOfTwo(x1), powerOfTwo(x2))}
println(gcdt(25,5))
고차 함수의 사용
- 코드를 작성할 때 자주 사용되는 패턴을 추상화하기 위해 고차 함수를 사용한다
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]
}
fun <T> Collection<T>.head() = first()
fun <T> Collection<T>.tail() = drop(1)
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)
}
}
콜백 리스너 대체
- 고차함수와 커링사용시 가독성 개선
- 체이닝으로 단계마다 평가되는것이 아니라 필요한 시점에서 게으르게 평가 됨
{ v2 ->
{ v3 ->
{ v4 ->
{ v5 ->
v1 + v2 + v3 + v4 + v5
}
}
}
}
}
val result = callback("1")("2")("3")("4")("5")
println(result) //12345
val result1 = callback("1")("2")
println(result1("3")("4")("5")) //12345
- 커링을 이용해서 부분 적용 함수로 재사용성을 높임
println(partialApp("1")("2")("3"))
println(partialApp("a")("b")("b"))
*개인적으로 코틀린을 공부하면서 정리한 자료입니다. 수정 사항 및 이슈가 있는 경우 메일 부탁드립니다.
댓글
댓글 쓰기