함수형 프로그래밍 in 코틀린 - 모노이드[9]
abstract : 모노이드 정의, 모노이드 타입 클래스 구현, 모노이드의 법칙 등
모노이드란?
모노이드는 연관 바이너리 함수와 항등값을 가진 대수적 타입으로 정의
연관 바이너리 함수 (곱셈, 덧셈, 리스트를 합치는 함수 대상) 두 개의 매개변수와 리턴값의 타입이 동일한 바이너리 함수 곱셈, 덧셈, 리스트 결합은 결합 법칙을 만족함
모노이드는 곱셈이나, 덧셈, 리스트 결합하기의 예와 같은 몇 가지 동작을 일반화 할 수 있고 이러한 동작은 함수형 프로그래밍에서 다루는 대부분의 대수적 타입에서 사용된다.
모노이드 타입 클래스 구현
간단한 모노이드 타입 클래스
memty 함수 : 항등원 리턴 mappend 함수 : 바이너리 함수 리턴
interface Monoid<T> {
fun mempty(): T
fun mappend(m1: T, m2: T): T
}
두 함수는 모노이드를 반환하는 것이 아니라, 모노이드가 가진 값의 타입을 반환하며 코드 체이닝을 할 수 없음 체이닝을 가능하도록 처리 하려면 코드가 복잡해지며 대수 타입 이해 학습을 위해 위와 같은 방식으로 사용
덧셈 타입 모노이드
fun main() {
val x = 1
val y = 2
val z = 3
SumMonoid().run {
println(mappend(mempty(), x) == x) // true
println(mappend(x, mempty()) == x) // true
println(mappend(mappend(x, y), z) == mappend(x, mappend(y, z))) // true
}
println(SumMonoid().mconcat(funListOf(1, 2, 3, 4, 5))) // 15
}
class SumMonoid : Monoid<Int> {
override fun mempty(): Int = 0 // 덧셈의 항등원은 0
override fun mappend(m1: Int, m2: Int): Int = m1 + m2
}
곱셈 타입 모노이드
fun main() {
val x = 1
val y = 2
val z = 3
ProductMonoid().run {
println(mappend(mempty(), x) == x) // true
println(mappend(x, mempty()) == x) // true
println(mappend(mappend(x, y), z) == mappend(x, mappend(y, z))) // true
}
println(ProductMonoid().mconcat(funListOf(1, 2, 3, 4, 5))) // 120
}
class ProductMonoid : Monoid<Int> {
override fun mempty(): Int = 1
override fun mappend(m1: Int, m2: Int): Int = m1 * m2
}
모노이드의 법칙
모노이드는 항등값과 바이너리 함수를 갖으며, 항등 법칙과 결합법칙을 만족해야 함
mppend(menpty(), x) = x // 항등 법칙 만족
mppend(x, menpty()) = x // 결합 법칙 만족
mappend(mappend(x, y), z) = mappend(x, mappend(y, z))
mconcat 함수 만들기
입력 받은 리스트 요소를 하나의 값으로 출력하는 함수
fun <T> Monoid<T>.mconcat(list: FunList<T>): T = list.foldRight(mempty(), ::mappend)
fun main(args: Array<String>){
prinln(ProductMonoid().mconcat(funListOf(1, 2, 3, 4, 5))) // 120
prinln(SumMonoid().mconcat(funListOf(1, 2, 3, 4, 5))) // 15
}
메이비 모노이드 만들기
메이비 모노이드는 실패 할 수 있는 연산의 결과를 모노이드로 처리 할 때 사용된다.
fun main() {
val x = Just(1)
val y = Just(2)
val z = Just(3)
MaybeMonoid.monoid(ProductMonoid()).run {
println(mappend(mempty(), x) == x) // true
println(mappend(x, mempty()) == x) // true
println(mappend(mappend(x, y), z) == mappend(x, mappend(y, z))) // true
}
MaybeMonoid.monoid(SumMonoid()).run {
println(mappend(mempty(), x) == x) // true
println(mappend(x, mempty()) == x) // true
println(mappend(mappend(x, y), z) == mappend(x, mappend(y, z))) // true
}
}
object MaybeMonoid {
fun <T> monoid(inValue: Monoid<T>) = object : Monoid<Maybe<T>> {
override fun mempty(): Maybe<T> = Nothing
override fun mappend(m1: Maybe<T>, m2: Maybe<T>): Maybe<T> = when {
m1 is Nothing -> m2
m2 is Nothing -> m1
m1 is Just && m2 is Just -> Just(inValue.mappend(m1.value, m2.value))
else -> Nothing
}
}
}
sealed class Maybe<out T>
data class Just<T>(val value: T) : Maybe<T>()
object Nothing : Maybe<kotlin.Nothing>()
모노이드를 통한 폴더블 이진 트리
fun main() {
val tree = Node(1,
Node(2,
Node(3), Node(4)),
Node(5,
Node(6), Node(7)))
println(tree.foldLeft(0) { a, b -> a + b }) // 28
println(tree.foldLeft(1) { a, b -> a * b }) // 5040
println(tree.foldMap({ a -> a * 2 }, SumMonoid())) // 56
println(tree.foldMap({ a -> a + 1 }, ProductMonoid())) // 40320
}
sealed class BinaryTree<out A> : Foldable<A> {
override fun <B> foldLeft(acc: B, f: (B, A) -> B): B = when (this) {
is EmptyTree -> acc
is Node -> {
val leftAcc = leftTree.foldLeft(acc, f)
val rootAcc = f(leftAcc, value)
rightTree.foldLeft(rootAcc, f)
}
}
}
data class Node<A>(val value: A, val leftTree: BinaryTree<A> = EmptyTree, val rightTree: BinaryTree<A> = EmptyTree) : BinaryTree<A>()
object EmptyTree : BinaryTree<kotlin.Nothing>()