Functor(펑터)와 Monad(모나드) - 1

스칼라 언어를 주로 사용하고 있는 입장에서 펑터나 모나드와 같은 용어를 종종 접하게 됩니다. 그래서 이번 포스트에서는 펑터와 모나드가 무엇인지에 대해 살펴보겠습니다. 일반적으로 대부분의 프로그래머들은, 특히 함수형 언어로 개발을 하지 않는 개발자들은, 모나드란 단어를 보았을 때, 자신과는 무관한 컴퓨터 프로그래밍 개념이고 상관없는 것이라고 생각하고 그냥 지나칠 수 있습니다. 사실 이러한 개념들은 이미 우리가 개발을 하면서 많이 접했었지만 인지를 하지 못한 것일 뿐입니다. 일단 모나드가 무엇인지 이해를 하면 다른 목적을 가진 서로 무관한 클래스나 추상화 개념들이 친숙하게 느껴질 것입니다.

펑터(Functor)

펑터는 어떤 값을 감싸고 있는 타입 파라미터를 가지는 자료구조입니다. 여기서 어떤 값을 감싸고 있는 타입이라는 것은 값을 감싸고 있는 컨텐스트나 컨테이너의 타입을 이야기 합니다. 해당 타입을 간단하게 그림으로 살펴보면 아래와 같습니다.

스칼라로 정의한 펑터 trait은 아래와 같습니다.

trait Functor[F[_]] {
    def map[A,B](fa: F[A])(f: A => B): F[B]
}

이 trait은 타입 파라미터 F[_]을 갖습니다. 이 F[_]는 위에서 이야기한 어떤 값을 감싸고 있는 컨테이너입니다. 펑터 trait에서 제공하는 유일한 연산은 map 함수 입니다. 이 map은 인자로 컨테이너와 이 컨테이너에 담긴 값을 변경하는 함수를 전달해줍니다. 그리고 그 결과를 다시 새로운 컨테이너로 포장합니다. 여기서 새로운 컨테이너라는 의미는 기존의 컨테이너의 값을 변경하는 것이 아닌 새로운 객체를 만들어 반환한다는 의미입니다. 펑터는 identity(항등) 함수를 전달하면 어떠한 다른 동작을 하지 않고 항상 같은 인스턴스를 반환해야 합니다. 즉 다음 예제와 같습니다.

def identity[A](x: A): A = x

map(x)(identity) == x

펑터의 F[_]의 인스턴스를 이용하는 유일한 방법은 값을 변형시키는 것이지만 기존의 map 함수에는 값을 꺼내는 방법이 정의되어 있지 않습니다. 예를 들어 getter나 setter와 같은 방법들이 따로 정의되어 있지 않습니다. 실제 값은 펑터라는 컨텍스트 속에 항상 존재합니다. 이것만으로는 펑터가 왜 필요한 것이고, 어디에 쓰이는지 알기 어렵습니다. 몇가지 예시를 통해 펑터에 대해 살펴보도록 하겠습니다.

case class Container[A](value: A)
val containerFunctor = new Functor[Container] {
    def map[A,B](ai: Container[A])(f: A => B): Container[B] = Container(f(ai.value))
}

간단한 펑터의 예입니다. 우리가 이 펑터를 통해 할 수 있는 것은 map 함수를 이용하여 Container 안에 존재하는 값을 변형하는 것입니다. 그 값을 꺼낼 수는 없습니다. 펑터를 이용하는 방법은 바로 타입 안정성을 유지하면서 값을 변형하는 것입니다.

val containerString = Container("abc")
val containerInt = containerFunctor.map(containerString)(_.length)

그러나 위와 같이 펑터를 정의하게 되면 펑터의 결과에 이어 다시 map 함수를 이용하고 싶은 경우에 조금 불편합니다.

val containerString = Container("abc")
val containerInt = containerFunctor.map(containerFunctor.map(containerString)(_.length))(_ + 3)

위와 같이 결과에 다시 펑터의 map 함수를 사용하는 형태입니다. 이와 같은 경우 펑터에 여러 함수를 적용하고 싶은 경우에 보기 어려운 문제가 있습니다. 이것을 함수의 합성하듯이 사용하려면 다음과 같이 선언해주면 됩니다.

object ToFunctorOps0 {
  class FunctorOps[F[_], A](val self: F[A])(implicit val F: Functor[F]) {
    def map[B](f: A => B): F[B] = F.map(self)(f)
  }

  implicit def ToFunctorOps[F[_],A](v: F[A])(implicit F: Functor[F]): FunctorOps[F, A] = new FunctorOps[F, A](v)
}

위와 같이 스칼라의 implicit를 사용하면 내가 선언한 Container 클래스 내에 map 함수가 존재하는 것처럼 호출이 가능합니다. 자세한 문법적인 설명은 따로 설명하지 않겠습니다. 이제 펑터를 이용하여 함수를 합성하듯이 사용할 수 있습니다.

val containerBytes = Container(customer)
    .map(_.getAddress)
    .map(_.getStreet)
    .map(_.substring(0, 3))
    .map(_.toLowerCase)
    .map(_.getBytes)

Container 펑터를 이용하면 값들을 맵핑하는 것이 함수 체이닝과 다르지 않아 보입니다.

val bytes = customer
    .getAddress()
    .getStreet()
    .substring(0, 3)
    .toLowerCase()
    .getBytes()

위의 예제를 보면 함수 체이닝과 크게 차이가 나지 않는데 왜 펑터라는 것이 필요로 할까요? 실제로 펑터를 이용해 추상화를 하면 여러 개념들을 모델링할 수 있습니다. 대표적인 펑터인 Option을 통해 살펴보겠습니다.

implicit 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))
  }
}

스칼라에서 Option은 타입 안정성을 보장해주면서 null 값을 표현해주는 방법입니다. Option 펑터의 경우 Option 값이 비어있는 경우 함수 f를 사용하지 않는 것이 특징입니다. 마찬가지로 fa 값은 변경되지 않고 그 안에 있는 값만 활용합니다. 펑터는 ContainerOption처럼 하나의 값만을 사용하는 컨테이너 뿐만 아니라 List와 같은 컬렉션도 컨테이너에 포함해서 사용할 수 있습니다. 위에서 선언한 타입 생성자인 F[_]가 컬렉션도 될 수 있다는 것입니다.

implicit val listFunctor: Functor[List] = new Functor[List] {
  def map[A, B](fa: List[A])(f: A => B) = fa map f
}

위의 코드를 보면 이제 List안의 모든 값에 f 함수를 적용할 수 있습니다. 예를 들어 위의 예제에서 사용했던 Customer의 리스트인 List[Customer]가 있고 리스트 안에 customer들의 주소의 도로명을을 알고 싶은 경우 다음과 같이 간단히 얻을 수 있습니다. (아래의 예제는 List 펑터 자체에 대해서 map을 사용할 수 있는 implicit 처리가 되어 있다라고 가정합니다.)

List(cutomer1, customer2, customer3)
  .map(_.getAddress)
  .map(_.getStreet)

결과는 List(street1, street2, stree3)이 될 것입니다. 이 경우 위에서 처럼 함수의 체이닝을 통해서는 작업할 수 없습니다.

List(cutomer1, customer2, customer3)
  .getAddress()
  .getStreet()

바로 위에 처럼 getAddress를 컬렉션에 대해 호출할 수 없고 개별 customer에 대해 호출을 해주고 다시 컬렉션에 넣어야 합니다. 펑터는 여러 자료구조를 추상화하여 내부를 감추고 사용하기 쉬운 일관된 API를 제공합니다. 이제 펑터의 효용성에 대해 조금을 이해했을 것이라고 생각합니다.

다음 포스트에서는 이어서 모나드에 대해 살펴보도록 하겠습니다.

References



comments powered by Disqus