Invariant Functor Pattern

圏論に Invariant Functor という概念があります。 ざっくりとプログラミングの概念で表現すると、Invariant Functor は、型コンストラクタ F、任意の型 A, B があったときに、型コンストラクタ F が f: A => B, g: B => A の2つの関数を定義できるような型コンストラクタ F のことを指します。 詳しいことは理解してなくても Invariant Functor は有用なのでコード例を紹介します。

まず、このパターンをどういう時に使えばいいかという疑問があるかと思いますが、例えば JSON の値を言語組込みの型と相互変換したい時に使います。

sealed trait JsValue
case class JsString(unwrap: String) extends JsValue
case class JsNumber(unwrap: BigDecimal) extends JsValue
case object JsNull extends JsValue

より具体的に言えば、このような JsValue 型があったとして、JsValue <=> String の変換が当てはまります。

では、次に Invariant Functor を実際に定義する方法を見ていきましょう。 Invariant Functor を Scala のコードにすると以下のようになります。

trait InvariantFunctor[F[_]] {
  def inmap[A, B](fa: F[A], f: A => B, g: B => A): F[B]
}

型自体は複雑ではないのでなんとなく分かってもらえると思います。

さて、ここで JsValue 型と任意の型を相互変換する Marshal 型をどうやって定義すればよいか考えます。 Marshal 型は、

trait Marshal[A] {
  def encode(x: A): JsValue
  def decode(json: JsValue): A
}

というインターフェースを持つ型クラスです。

Marshal[String] を定義する方法を見ましょう。

trait MarshalInstances {

  def of[A](implicit M: Marshal[A]): Marshal[A] = M

  private[this] def encodeNullable[A](x: A): Option[A] =
    Option(x)

  private[this] def decodeNullable[A](x: Option[A]): A =
    x match {
      case Some(v) => v
      case None    => null.asInstanceOf[A]
    }

  implicit val marshalInvariantFunctor: InvariantFunctor[Marshal] =
    new InvariantFunctor[Marshal] {
      override def inmap[A, B](fa: Marshal[A], f: A => B, g: B => A): Marshal[B] =
        new Marshal[B] {
          override def encode(x: B): JsValue = fa.encode(g(x))
          override def decode(json: JsValue): B = f(fa.decode(json))
        }
    }

  implicit class MarshalOps[A](fa: Marshal[A])(implicit F: InvariantFunctor[Marshal]) {
    def inmap[B](f: A => B, g: B => A): Marshal[B] = F.inmap(fa, f, g)
  }

  implicit val marshalOfString: Marshal[String] =
    new Marshal[String] {
      override def encode(x: String): JsValue = JsString(x)
      override def decode(json: JsValue): String =
        json match {
          case JsString(x) => x
          case _ => null.asInstanceOf[String]
        }
    }

  implicit val marshalOfOptionString: Marshal[Option[String]] =
    of[String].inmap(encodeNullable, decodeNullable)
}

syntax sugar の導入のためにだいぶごちゃごちゃしていますが、上記コードのポイントは、

implicit val marshalOfOptionString: Marshal[Option[String]] =
  of[String].inmap(encodeNullable, decodeNullable)

の箇所になります。 このコードはただ Marshal[String] から Marshal[Option[String]] へ変換しているだけですが、その時に InvariantFunctor.inmap を使っていて、encodeNullabledecodeNullable という汎用的な関数を利用することで Marshal[A] から Marshal[Option[A]] を機械的に導出することが可能になっています。

String と似たような例ですが BigDecimal の変換の場合は、

  implicit val marshalOfBigDecimal: Marshal[BigDecimal] =
    new Marshal[BigDecimal] {
      override def encode(x: BigDecimal): JsValue = JsNumber(x)
      override def decode(json: JsValue): Number =
        json match {
          case JsNumber(x) => x
          case _ => null.asInstanceOf[BigDecimal]
        }
    }

  implicit val marshalOfOptionBigDecimal: Marshal[Option[BigDecimal]] =
    of[BigDecimal].inmap(encodeNullable, decodeNullable)

のように定義できます。

Invariant Functor は、play-jsonscalikejdbc でも使われているので、興味があれば参考にしてください。

Comments

comments powered by Disqus