욱'S 노트

모나드 소개/Introduction of Monad (Kotlin 함수형 프로그래밍 #8) 본문

Methdology/Functional Programming

모나드 소개/Introduction of Monad (Kotlin 함수형 프로그래밍 #8)

devsun 2025. 1. 22. 10:56
반응형

일단 모나드에 대한 용어에 신경쓰지 말고 시작해보자.

모나드가 없을 때

간단한 Either를 하나 작성해보자.

sealed interface Either<out A, out B> {
    data class Left<A>(val error: A) : Either<A, Nothing>
    data class Right<B>(val value: B) : Either<Nothing, B>
}

 

계좌의 입출금 문제를 생각해보자

sealed interface Error {
    data object NegativeAmount : Error
}

data class Account private constructor(val balance: BigDecimal) {
    companion object {
        fun create(initial: BigDecimal): Either<Error, Account> {
            return if (initial < BigDecimal.ZERO) Left(NegativeAmount)
            else Right(Account(initial))
        }
    }

    fun deposit(amount: BigDecimal): Either<Error, Account> {
        return if (amount < BigDecimal.ZERO) Left(NegativeAmount)
        else Right(Account(balance + amount))
    }

    fun withdraw(amount: BigDecimal): Either<Error, Account> {
        return if (amount < BigDecimal.ZERO) Left(NegativeAmount)
        else Right(Account(balance - amount))
    }
}

 

 

일반적으로 입출금을 수행하는 코드를 작성해보면 다음과 같다.

val accountResult = Account.create(100.toBigDecimal())

val depositResult = when (accountResult) {
    is Right -> accountResult.value.deposit(100.toBigDecimal())
    is Left -> Left(accountResult.error)
}

val withdrawResult = when (depositResult) {
    is Right -> depositResult.value.withdraw(50.toBigDecimal())
    is Left -> Left(depositResult.error)
}

withdrawResult shouldBe Right(50.toBigDecimal())

 

모나드 적용

모나드의 인터페이스는 다음과 같다. 모나드의 특징은 합성 가능하다는 것이다. 그래서 flatMap 연산이 가능하다.

interface Monad<A> {
    fun <B> flatMap(f: (A) -> Monad<B>): Monad<B>
    fun <A> unit(a: A): Monad<A>
}

 

두가지 연산을 Either에 추가해보자.

sealed interface Either<out A, out B> {
    data class Left<A>(val error: A) : Either<A, Nothing>
    data class Right<B>(val value: B) : Either<Nothing, B>

    fun <A, C> flatMap(fn: (B) -> Either<A, C>): Either<A, C> = when (this) {
        is Right -> fn(this.value)
        is Left -> this as Either<A, C>
    }

    fun <A, B> unit(b: B): Either<A, B> = Right(b)
}

 

위의 출금 연산을 다시 작성해보면 아래와 같다.

Account.create(100.toBigDecimal())
    .flatMap { it.deposit(50.toBigDecimal()) }
    .flatMap { it.withdraw(30.toBigDecimal()) } shouldBe Right(50.toBigDecimal())

 

모나드로 할 수 있는 것

모나드의 가장 좋은 점은 합성 가능하다는 것이다. 합성 가능하다는 특성으로 아래와 같은 일들을 수행할 수 있다.

  • Nullability: Maybe/Option monad
  • Error Handling: Either monad
  • DI (Dependency Injection): Reader monad
  • Logging: Writer monad
  • Side Effects : IO monad
  • State handling: State monad
  • Collections: List monad
  • Async Handling : Future monad
  • Parallel Processing: Par Monad

State Monad

data class State<S, out A>(val run: (S) -> Pair<A, S>) {
    fun <B> flatMap(f: (A) -> State<S, B>): State<S, B> =
        State { s0 ->
            val (a, s1) = run(s0)
            f(a).run(s1)
        }

    fun <B> map(f: (A) -> B): State<S, B> = flatMap { a -> State { s -> Pair(f(a), s) } }
}

// Usage example
data class CounterState(val count: Int)

fun incrementCounter(): State<CounterState, Unit> = State { state ->
    Pair(Unit, state.copy(count = state.count + 1))
}

fun doubleCounter(): State<CounterState, Unit> = State { state ->
    Pair(Unit, state.copy(count = state.count * 2))
}

val result: Pair<Unit, CounterState> = incrementCounter().flatMap { doubleCounter() }.run(CounterState(0))

 

Reader 모나드

typealias Reader<E, A> = (E) -> A

fun <E, A, B> Reader<E, A>.flatMap(transform: (A) -> Reader<E, B>): Reader<E, B> =
    { env -> transform(this(env))(env) }

fun <E, A, B> Reader<E, A>.map(transform: (A) -> B): Reader<E, B> =
    { env -> transform(this(env)) }

// Usage example
data class AppConfig(val apiUrl: String)

fun fetchUser(userId: String): Reader<AppConfig, String> =
    { config -> "User $userId fetched from ${config.apiUrl}" }

fun logUser(user: String): Reader<AppConfig, Unit> =
    { config -> println("Logging user: $user to ${config.apiUrl}") }

val program: Reader<AppConfig, Unit> =
    fetchUser("123").flatMap { user ->
        logUser(user)
    }

// Running the program
val config = AppConfig("https://api.example.com")
program(config)

 

Future Monad

suspend fun <T> asyncOperation(): T = suspendCoroutine { continuation ->
    // Simulating an async operation
    GlobalScope.launch {
        delay(1000) // Simulating a delay
        val result = /* Perform some computation */
            continuation.resume(result)
    }
}

fun <T, R> Deferred<T>.flatMap(transform: (T) -> Deferred<R>): Deferred<R> =
    GlobalScope.async { transform(await()).await() }

fun <T, R> Deferred<T>.map(transform: (T) -> R): Deferred<R> =
    GlobalScope.async { transform(await()) }

// Usage example
val result: Deferred<Int> = asyncOperation<Int>().flatMap { value ->
    asyncOperation<Int>().map { value + it }
}
runBlocking { println(result.await()) }

 

Par Monad

@extension
interface ParMonad : Monad<ForPar> {
    override fun <A> unit(a: A): ParOf<A> = Par.unit(a)

    override fun <A, B> flatMap(
        fa: ParOf<A>,
        f: (A) -> ParOf<B>
    ): ParOf<B> =
        fa.fix().flatMap { a -> f(a).fix() }
}

 

도대체 모나드란 무엇인가?

모나드는 결합 법칙과 항등원 법칙을 만족하는 모나드적인 콤비네티어의 최소 집합중 하나를 구현한 것이다. 모나드의 인스턴스는 세가지 집합 중 어느 하나를 제공해야만 한다.

  • flatMap, unit
  • compose, unit
  • map, join, unit

모나드 법칙

왼쪽 항등법칙(Left Identity)

  • 모나드가 값을 감싸는 방식이 함수를 직접 적용하는 것과 동일하게 작동해야 한다.
pure(x).flatMap(f) == f(x)
Option(1).flatMap(f) == Option(f(1))
None.flatMap(f) == None

 

오른쪽 항등법칙(Right Identity)

  • 모나드에 pure를 적용했을때 결과가 모나드 자신과 동일해야 된다.
m.flatMap(pure) == m
Option(1).flatMap(pure) == Option(1)
None.flatMap(pure) == None

 

결합법칙

  • 모나드 연산의 순서가 결과에 영향을 미치치 않아야 한다.
(m.flatMap(f)).flatMap(g) == m.flatMap { x → f(x).flatMap(g) }

 

정리

  • 모나드는 항등원 법칙과 결합법칙을 만족하여 합성 가능하다.
  • 모나드는 합성가능하다.
  • 모나드의 합성가능함으로써 코드의 안정성과 가독성을 높이고, 동시성과 병렬성 처리에 용이하다.

 

참고자료:  https://www.dak.so/eb0005f6-5175-41ca-942a-56868f55f21c

 

모나드의 장점

cleanUrl: "/monad/pros"

www.dak.so

 

https://www.yes24.com/Product/Goods/120236288?pid=123487&cosemkid=go16903580741098008&utm_source=google_pc&utm_medium=cpc&utm_campaign=book_pc&utm_content=ys_240530_google_pc_cc_book_pc_12306%EB%8F%84%EC%84%9C&utm_term=%EC%BD%94%ED%8B%80%EB%A6%B0%ED%95%A8%EC%88%98%ED%98%95%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&gad_source=1&gclid=CjwKCAiAm-67BhBlEiwAEVftNp4rJHRMrNaBE2YyQCT-r4Gxu_X9K6NdakIvehx_Mxsi48Jj3rZ45BoCbUsQAvD_BwE

 

코틀린 함수형 프로그래밍 - 예스24

함수형 프로그래밍을 들어본 개발자는 많지만 제대로 된 함수형 프로그래밍을 배우기 위해 하스켈이나 스칼라까지 배울 여유가 없는 독자도 많을 것이다. 이 책은 정석적인 함수형 프로그래밍

www.yes24.com

 

 

반응형