[小ネタ] catsでFunctorの合成

catsでのFunctorの合成をやってみました。
2022.06.29

[小ネタ]catsでFunctorの合成

はじめに

Category Theory for Programmers (以下CTFP)の「7.3 Functor Composition」でファンクタの合成について記述されていたのでそれをScalaで確かめてみました。

CTFPでの例

square x = x * x

mis :: Maybe [Int]
mis = Just [1, 2, 3]
-- リストの中身を写したい
mis2 = fmap (fmap square) mis
-- 関数合成でも書ける
mis2 = (fmap . fmap) square mis

Scalaで書く

素直にScalaで書くと以下のようになります。catsのfmapはHaskellとは引数の順序が逆なのでfmap2を定義しておきます。

def fmap2[F[_]: Functor, A, B](f: A => B)(fa: F[A]): F[B] = Functor[F].fmap(fa)(f)

これを使うと以下のようになります。

def square(x: Int): Int = x * x
def mis: Option[List[Int]] = Some(List(1, 2, 3))
def mis2_1: Option[List[Int]] = fmap2(fmap2[List, Int, Int](square))(mis)
def mis2_2 =(fmap2[Option, List[Int], List[Int]] _ compose fmap2[List, Int, Int])(square)(mis)

関数合成を使うバージョンはかなり冗長です。

mis2_1 のバージョンでlaw testingしてみます。

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

checkAll(
  "OptionList[Int].FunctorLaws",
  FunctorTests[OptionList].functor[Int, Int, String]
)
// [info] FunctorCompositionTest:
// [info] - OptionList[Int].FunctorLaws.functor.covariant composition
// [info] - OptionList[Int].FunctorLaws.functor.covariant identity
// [info] - OptionList[Int].FunctorLaws.functor.invariant composition
// [info] - OptionList[Int].FunctorLaws.functor.invariant identity

ComposeFunctor[F, G] を使う

catsにはComposeFunctorというのがあってFunctorの合成ができます。

private[cats] trait ComposedFunctor[F[_], G[_]] extends Functor[λ[α => F[G[α]]]] with ComposedInvariant[F, G] { outer =>
  def F: Functor[F]
  def G: Functor[G]

  override def map[A, B](fga: F[G[A]])(f: A => B): F[G[B]] =
    F.map(fga)(ga => G.map(ga)(f))
}

これを使うと以下のようになります。

def mis2 = (Functor[Option] compose Functor[List]).map(mis)(square)

通常は冗長なのでエイリアス定義しておくのが良さそうです。

type OptionList[A] = Option[List[A]]
implicit val functor: Functor[OptionList] =
    Functor[Option] compose Functor[List]

まとめ

OptionTに積むよりネストした方がシンプルなときもあるので、合成したファンクタのエイリアスと一緒にファンクタのインスタンスを定義しておくのがいいかなと思いました。