HaskellとcatsのApplicative Functorについて考えてみた

cats.ApplicativeがなんなのかHaskellの入門書である「すごいHaskellたのしく学ぼう!」を読みつつ考えてみました。
2020.06.23

はじめに

cats.Applicativeの使い道がいまいちわからないのですごいH本を参考に考えてみました。

Applicative

まずcatsでのApplicativeの定義は下記です(抜粋)。

trait Applicative[F[_]] extends Functor[F] {
  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

  def pure[A](a: A): F[A]

  def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa)
}

apについてすごいH本では下記のような例が紹介されています。<*>によってApplicativeを扱っていない関数をApplicative値に適用できます(<*>がcatsでいうap)

Prelude Control.Applicative> (pure (+)) <*> Just 1 <*> Just 2
Just 3

catsでも同様に<*>apのエイリアスとして定義されていて、同等の使い方ができます。

@ import cats.Applicative
import cats.Applicative

@ Applicative[Option].pure { a:Int => b:Int => a+b } <*> 1.some <*> 2.some
res6: Option[Int] = Some(3)

さて、HaskellのApplicativeは以下の演算子<$>をエクスポートしています。

(<$>) :: (Functor f) => (a -> b) -> f a > f b
f <$> x = fmap f x

これを使うと先ほどの例は以下のようになります。

Prelude Control.Applicative> (+) <$> Just 1 <*> Just 2
Just 3

部分適用することもできます。

Prelude Control.Applicative> :t ((+) <$> Just 1)
((+) <$> Just 1) :: Num a => Maybe (a -> a)

catsではmapNがこれに相当するように見えます。これを使うと<$>のように部分適用もできます。しかし実際のところmapNSemigroupal#mapN(n = {2....N})のエイリアスで、型も異なっています。

@ import cats.implicits._
import cats.implicits._
//mapN
@ (1.some, 2.some).mapN(_+_)
res1: Option[Int] = Some(3)
//map2
@ Applicative[Option].map2(1.some, 2.some)(_+_)
res5: Option[Int] = Some(3)
//部分適用
@ (1.some, _:Option[Int]).mapN(_+_)
res17: Option[Int] => Option[Int] = ammonite.$sess.cmd17$$$Lambda$1873/1950478035@7432cb37
@ (Applicative[Option].map2(1.some, _:Option[Int])(_+_))
res19: Option[Int] => Option[Int] = ammonite.$sess.cmd19$$$Lambda$1896/649325384@77f21c0a

そこでmap2map3の定義を見るとSemigroupal#productが使われているのがわかります。

def map2[F[_], A0, A1, Z](f0:F[A0], f1:F[A1])(f: (A0, A1) => Z)(implicit semigroupal: Semigroupal[F], functor: Functor[F]): F[Z] =
    functor.map(semigroupal.product(f0, f1)) { case (a0, a1) => f(a0, a1) }

def map3[F[_], A0, A1, A2, Z](f0:F[A0], f1:F[A1], f2:F[A2])(f: (A0, A1, A2) => Z)(implicit semigroupal: Semigroupal[F], functor: Functor[F]): F[Z] =
    functor.map(semigroupal.product(f0, semigroupal.product(f1, f2))) { case (a0, (a1, a2)) => f(a0, a1, a2) }

<$>mapNは似ていますがやはり別物に見えます。

ここでproductSemigroupalにおいて以下のように定義されています。

@typeclass trait Semigroupal[F[_]] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}

productfmapapで実装してみます。

implicit def semigroupal[F[_] : Applicative]: Semigroupal[F] = new Semigroupal[F] {
  override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] = Functor[F].fmap(fa) { a: A => b: B => (a, b) } <*> fb
}

これは タプルを扱っているものの<$><*>を組み合わせて使ったときと同じです。

まとめ

Applicativeを使うことでApplicativeを扱わない関数をApplicative値に適用できることがわかりました。