Fold 란?
fold란 고차 함수중 하나입니다.
재귀적으로 정의된 자료구조를 분석하고, 전달받은 명령을 사용하여 재결합하고, 재귀적으로 수행된 결과들로 반환값을 만들어내는 연산입니다.
Fold (고차 함수) - 위키백과, 우리 모두의 백과사전 (wikipedia.org)
하나하나 의미를 살펴봅시다.
재귀적으로 정의된 자료구조
sealed class List<out A> {
companion object {
fun <A> of(vararg aa: A): List<A> {
val tail = aa.sliceArray(1..<aa.size)
return if (aa.isEmpty()) Nil else Cons(aa[0], of(*tail))
}
}
}
object Nil : List<Nothing>()
data class Cons<out A>(val head: A, val tail: List<A>) : List<A>()
Cons는 멤버변수로 List 타입을 갖기 때문에 재귀적인 자료구조라고 할 수 있습니다.
전달받은 명령을 사용하여 재결합
fun <A, B> foldRight(xs: List<A>, z: B, f: (A, B) -> B): B =
when (xs) {
is Nil -> z
is Cons -> f(xs.head, foldRight(xs.tail, z, f))
}
fun main() {
val list = Cons(3, Cons(6, Cons(9, Nil)))
foldRight(list, 0) { a, b ->
a + b
}
}
foldRight함수에 명령으로 { a, b -> a + b } 람다를 전달했습니다.
foldRight안에서는 f(xs.head, foldRight(xs.tail, z, f)) 를 통해 재결합 하고 있습니다.
재귀적으로 수행된 결과들로 반환값을 만들어 낸다
is Cons -> f(xs.head, foldRight(xs.tail, z, f))
f연산은 { a, b -> a + b }입니다.
xs.head + foldRight(xs.tail, z, f) 를 결과로 반환합니다.
xs.head + (xs.tail.head + foldRight(xs.tail.tail, z, f))
xs.head + (xs.tail.head + (xs.tail.tail.head + foldRight(xs.tail.tail.tail, z, f))) ...
이렇게 재귀적으로 계산되다가 Nil 조건을 만나면 반환값을 반들어냅니다.
Fold 방향
fold는 "재귀적으로 수행" 을 어떤 방향으로 적용하는 지에 따라 FoldLeft와 FoldRight가 나뉘어집니다.
- FoldLeft
- 요소의 앞에서 부터 계산
- (((1 + 2) + 3) + 4) +5
- FoldRight
- 요소의 뒤에서 부터 계산
- 1 + (2 + (3 + (4 + 5)))
FoldLeft
sealed class List2<out A> {
companion object {
fun <A> of(vararg aa: A): List2<A> {
val heads = aa.sliceArray(0..<aa.size - 1)
return if (aa.isEmpty()) Nil2 else Cons2(of(*heads), aa.last())
}
}
}
object Nil2 : List2<Nothing>()
data class Cons2<out A>(val heads: List2<A>, val last: A) : List2<A>()
fun <ITEM1, ITEM2> foldLeft(list: List2<ITEM1>, defaultValue: ITEM2, combine: (ITEM2, ITEM1) -> ITEM2): ITEM2 =
when (list) {
is Nil2 -> defaultValue
is Cons2 -> combine(foldLeft(list, defaultValue, combine), list.last)
}
foldLeft를 구현하기위해 List2 자료구조를 새로 만들었습니다.
// List
val list = Cons(3, Cons(6, Cons(9, Nil)))
// List2
val list1 = Cons2(Cons2(Cons2(Nil2, 1), 2), 3)
리스트를 정의할 때 차이점이 있습니다.
첫번째 요소에 도달할 때 까지 재귀적으로 foldLeft를 호출하게 됩니다.
List2를 새로 만들지 않고, foldLeft를 구현할 수 도 있습니다.
tailrec fun <ITEM1, ITEM2> foldLeft(list: List<ITEM1>, defaultValue: ITEM2, combine: (ITEM2, ITEM1) -> ITEM2): ITEM2 =
when(list) {
is Nil -> defaultValue
is Cons -> foldLeft2(list.tail, combine(defaultValue, list.head), combine)
}
첫 번째 요소부터, defaultValue에 누적하면서 마지막요소까지 계산해갑니다
위의 List2의 예시는 틀렸습니다.
앞에서 부터 계산하는지와 뒤에서 계산하는지와는 별개로 재귀적 호출된 함수의 결과를 언제 조합하냐가 핵심입니다.
먼저 계산한 후 다음 재귀 호출을 하면, Left
모든 재귀호출이 끝난 후 콜스택을 돌아오면서 계산을 마치면 Right입니다.
FoldRight
fun <ITEM1, ITEM2> foldRight(list: List<ITEM1>, defaultValue: ITEM2, combine: (ITEM1, ITEM2) -> ITEM2): ITEM2 =
when (list) {
is Nil -> defaultValue
is Cons -> combine(list.head, foldRight(list.tail, defaultValue, combine))
}
foldRight는 위에서 봤던 정의와 동일합니다.
마지막 요소를 만날 때까지 재귀적으로 호출되다가, 다시 돌아오면서 combine되는 형식입니다.
특징
꼬리 재귀
foldLeft는 꼬리 재귀로 만들 수 있지만, foldRight는 꼬리 재귀로 만들 수 없다.
결합 법칙 성립하지 않는 연산
- FoldLeft
- 요소의 앞에서 부터 계산
- (((1 + 2) + 3) + 4) +5
- FoldRight
- 요소의 뒤에서 부터 계산
- 1 + (2 + (3 + (4 + 5)))
위에서 이런 정의를 했었습니다.
이 정의에서는 + 연산을 하였기 때문에 foldLeft, foldRight 모두 같은 결과가 나왔습니다.
+연산은 결합법칙이 적용되기 때문인데, 결합 법칙이 적용되지 않는 ÷ 연산은 어떨까요?
4 -> 2 -> 2 리스트가 있다고 해보겠습니다.
- foldLeft : ((4 / 2) / 2) = 1
- foldRight : (4 / (2 / 2)) = 4
이렇게 결합 법칙이 적용되지 않는 연산은 foldLeft와 foldRight의 결과가 다를 수 있습니다.
'Programming > 코틀린 함수형 프로그래밍' 카테고리의 다른 글
6. 순수 함수형 상태 (0) | 2023.10.20 |
---|---|
5. 엄격성과 지연성 (0) | 2023.10.15 |
4. 예외를 사용하지 않고 오류 다루기 - Functional Programming in Kotlin (0) | 2023.09.10 |
3. 함수형 데이터 구조 - Functional Programming in Kotlin (0) | 2023.09.10 |
2. 코틀린으로 함수형 프로그래밍 시작하기 - Functional Programming in Kotlin (0) | 2023.09.06 |