Functor(펑터)와 Monad(모나드) - 2
이전 포스트에서는 펑터가 무엇인지에 관해 살펴보았습니다. 펑터는 유용한 추상화이지만 약간의 문제가 존재합니다. 펑터의 map
함수가 일반적인 값을 반환하는 경우에는 문제가 되지 않지만 만약 펑터를 반환하면 어떻게 될까요? 물론 펑터 역시 값이기 때문에 상관은 없겠지만 펑터를 반환하는 경우 결과가 조금 다르게 나올 수 있습니다. 다음 예제를 통해서 살펴보겠습니다.
val functorForOption: Functor[Option] = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa match {
case None => None
case Some(a) => Some(f(a))
}
}
def stringToInt(s: String): Option[Int] = {
try {
Some(s.toInt)
} catch {
case e: NumberFormatException =>
None
}
}
val opt: Option[Option[Int]] = {
functorForOption.map(Some("hello"))(x => stringToInt(x))
}
일단 위의 예제 코드에는 문법적으로 이상은 없습니다. stringToInt
함수가 Int
를 반환한다면 Option[Int]
가 될 것입니다. 그러나 펑터의 map
함수가 Option
을 리턴하기 때문에 위 예제의 opt
의 타입은 Option[Option[Int]]
이 됩니다. 위의 예제처럼 펑터가 펑터를 감싼 형태가 되지 때문에 합성과 체이닝을 힘들게 합니다. 한번 다음의 예제를 통해 살펴보겠습니다.
val intOpt = Option(10)
val intOptOpt = Option(Option(10))
val doubleOpt1 = intOpt.map(_.toDouble)
val doubleOpt2 = intOptOpt.map(_.toDouble) // compile error
위의 예제는 Option
안에 있는 Int
값을 Double
로 바꾸는 방법을 보여줍니다. Int => Double
로 구성된 함수만 있다면 Option[Int]
의 값을 Option[Double]
로 쉽게 바꿀 수 있습니다. 하지만 Option
이 두 번 감싸져 있는 intOptOpt
의 예제는 우리가 생각한대로 동작을 하지 않습니다. 펑터가 두 번 감싸져 있는 경우 펑터가 제 기능을 수행하기 어려워집니다. 하지만 리턴 값이 펑터인 경우는 일반적이기 때문에 이를 해결해야 합니다. 그래서 특별한 함수를 도입해서 중첩된 펑터를 납작(flat)하게 만드는 기능을 제공합니다.
val intOpt2: Option[Int] = intOptOpt.flatten
이렇게 되면 위에서 발생한 문제는 해결됩니다. 그러나 이렇게 사용되는 경우가 흔하기 때문에 펑터를 리턴하는 map
함수를 사용한 다음 매번 flatten
을 호출하는 것은 번거롭습니다. 그렇기 때문에 flatMap
이라는 특별한 함수를 도입합니다. flatMap
과 map
은 유사하지만 파라미터로 받는 함수가 펑터(정확히는 모나드입니다)를 반환합니다. 그러면 스칼라에서 모나드는 다음과 같이 선언합니다.
trait Monad[F[_]] extends Functor[F] {
def unit[A](a: => A): F[A]
def flatMap[A,B](ma: F[A])(f: A => F[B]): F[B]
}
flatMap
을 사용하면 첫 번째 예제인 stringToInt
을 사용하는 코드가 중첩되는 모나드의 형태가 아닌 하나의 모나드로 감싸져 있는 형태가 됩니다. 그러면 함수를 합성하거나 체이닝해서 사용하는 것이 한결 수월해 집니다. flatMap
만 있다고 해서 어떤 클래스를 모나드라고 할 수 없습니다. 모나드는 특정 법칙을 만족해야 합니다. 이 모나드 법칙은 다음과 같습니다.
모나드 법칙
1) 항등법칙
모나드는 항등법칙을 만족합니다. 모나드는 모나드에 대한 항등원을 가지고 있어야 하는데 그것을 정의한 것이 바로 unit
이라는 함수입니다. unit
함수의 정의는 다음과 같습니다.
def unit[A](a: => A): F[A]
임의의 모나드와 unit
의 합성은 원래 그 모나드와 같습니다. 이것은 2가지 법칙으로 표현되는데 왼쪽 항등법칙
과 오른쪽 항등법칙
입니다. 간단히 이 법칙들을 flatMap
으로 표현하면 다음과 같습니다.
def unit[A](a: A): Option[A] = Some(a)
def flatMap[A,B](ma: Option[A])(f: A => Option[B]): Option[B] = ma flatMap f
def f: Int => Option[Int] = a => Some(a * 2)
val x = 2
flatMap(unit(x))(f) == f(x) // true
flatMap(f(x))(unit) == f(x) // true
2) 결합법칙
모나드는 결합법칙을 만족해야 합니다. 마찬가지로 예제를 통해 살펴보겠습니다.
def unit[A](a: A): Option[A] = Some(a)
def flatMap[A,B](ma: Option[A])(f: A => Option[B]): Option[B] = ma flatMap f
val f: Int => Option[Int] = a => Some(a + 2)
val g: Int => Option[Int] = a => Some(a * 2)
val ma = Some(1)
val f: Int => Option[Int] = a => Some(1)
(ma flatMap f flatMap g) == (ma flatMap(a => f(a) flatMap g)) // true
위의 예제와 같이 모나드는 결합법칙을 만족합니다. 모나드는 어떤 일이 일어나도 그것이 결합법칙과 항등법칙을 만족한다는 것을 의미합니다.
모나드의 map
은 따로 구현할 필요가 없습니다. flatMap
으로 쉽게 구현할 수 있습니다.
def map[A,B](ma: F[A])(f: A => B): F[B] =
flatMap(ma)(a => unit(f(a)))
그럼 이러한 모나드는 왜 쓰는 것일까요? 간단한 예제를 통해서 모나드의 강력함을 살펴보도록 하겠습니다.
스칼라에서는 Future
모나드라는 것이 있습니다. 이 Future
는 미래의 어떤 값을 가진다는 의미입니다. 다음과 같이 정의된 함수들이 있습니다.
def loadCustomer(id: Int): Future[Customer]
def readBasket(customer: Customer): Future[Basket]
def calculateDiscount(basket: Basket, dayOfWeek: DayOfWeek): Future[Double]
위에서 정의된 함수 시그니처만 살펴보면 어떤 작업을 수행하는지 알 수 있습니다. 각각의 함수들은 Future[A]
를 리턴합니다. 그리고 그 함수는 완료하는데 일정 시간이 걸립니다. 위의 함수들을 모나드 연산자를 사용하면 마치 블로킹 호출처럼 함수들을 합성할 수 있습니다.
val discount = loadCustomer(513)
.flatMap(readBasket)
.flatMap(basket => calculateDiscount(basket, DayOfWeek.Friday))
모나드의 flatMap
은 모나드 타입을 유지해야 하므로 flatMap
의 결과가 전부 Future
입니다. 이렇게 코드를 작성을 하게 되면 코드들이 완전히 비동기적으로 동작하는 장점이 생깁니다. 즉, loadCustomer
를 호출하면 블로킹이 되지 않고 바로 Future
를 반환합니다. 그 다음 readBasket
역시 앞의 Future
값을 통해 새로운 Future
를 반환하기 됩니다. 모나드를 사용하여 간단하게 비동기 파이프라인을 구성한 것입니다.
이번 포스트에서는 함수형 프로그래밍에서 사용되는 중요한 개념인 펑터와 모나드에 대해 알아보았습니다. 함수형 프로그래밍 언어를 사용하면서 아마 인지하진 않았지만 펑터와 모나드를 사용하셨을 것입니다. 이번 포스트를 통해 펑터와 모나드에 대한 개념이 조금 더 친숙해졌으면 좋겠습니다.
References
- 스칼라로 배우는 함수형 프로그래밍
- (번역) Functor and monad examples in plain Java
- 스칼라 초중급자를 위한 모나드 해설
- Monad Programming with Scala Future
- Functors, Applicatives, And Monads In Pictures
- Functor of cats
- scala Monad