InvariantMonoidalって何?

catsのInvariant Monoidalをコードを書きながら確かめてみました。
2022.03.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

ScalaMatsuri 2022で拝見したとても興味深いセッションで「HKD[T, F]からF[T]を得るにはFがInvariantMonoidalである必要がある」という制約が出てきて、InvariantMonoidalが何なのか気になったのでCatsのドキュメントを読みながら手を動かして確認してみました。

前準備

この記事ではSemigroupを使っていくのでコンストラクタとLongに対するインスタンスを定義しておきます。

import cats._

import java.util.Date

trait SemigroupExample:
  def semigroup[A](f: (x:A, y:A) => A): Semigroup[A] = (x: A, y: A) => f(x,y)
  val longToDate: Long => Date = new Date(_)
  val dateToLong: Date => Long = _.getTime

  given semigroupLong: Semigroup[Long] = semigroup(_+_)

InvariantMonoidalはInvariantでありSemigroupal

まずドキュメントを見るとInvariantMonoidalはInvariantでありSemigroupalであることがわかります。

InvariantMonoidalcombines Invariant and Semigroupal with the addition of a unitmethods, defined in isolation the InvariantMonoidal

シグネチャは以下の通りなのですがそれぞれみていきます。

trait InvariantMonoidal[F[_]] {
  def unit: F[Unit]
  def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}

Invariant

Invariantのシグネチャは以下の通りです。Covariant FunctorそしてContravariant Functorを悪魔合体したような見た目です。

def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]

今、準備しておいたSemigroup[Long]を使ってSemigroup[Date]を作ることを考えます。もしSemigroupがCovariant Functorを形成する場合、Semigroup[Date]を作れるでしょうか?

おそらく以下のようなコードになりますが、combineしたBをAに戻す方法がないので手詰まりです。

given covariantSemigroup: Functor[Semigroup] = new Functor[Semigroup] {
    override def map[A, B](fa: Semigroup[A])(f: A => B): Semigroup[B] = semigroup[B]((x: A, y: A) => fa.combine(f(x), f(y)))
}

Contravariantの場合も同様です。

given contravariantSemigroup: Contravariant[Semigroup] = new Contravariant[Semigroup] {
      override def contramap[A, B](fa: Semigroup[A])(f: B => A): Semigroup[B] = semigroup((x:B, y:B) => fa.combine(f(x), f(y)))
}

Invariantならできます。

// invariantは定義できる
given invariantSemigroup: Invariant[Semigroup] = new Invariant[Semigroup]:
  override def imap[A, B](fa: Semigroup[A])(f: A => B)(g: B => A): Semigroup[B] = semigroup[B]((x:B, y:B) => f(fa.combine(g(x), g(y))))

そしてSemigroup[Date]も!!

//invariantでsemigroupを導けた!!
given semigroupDate: Semigroup[Date] = summon[Invariant[Semigroup]].imap(summon[Semigroup[Long]])(longToDate)(dateToLong)

Semigroupal

Semigroupal のシグネチャは以下です。SemigroupalではF[A], F[B]をF[(A, B)]に写します。

def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]

定義をそのまま述べるのに意味があるのか疑問が表面化する前に実装してみます。

given semigroupalSemigroup: Semigroupal[Semigroup] = new Semigroupal[Semigroup]:
  override def product[A, B](fa: Semigroup[A], fb: Semigroup[B]): Semigroup[(A, B)] = semigroup {
    case ((a1, b1), (a2, b2)) => (fa.combine(a1, a2), fb.combine(b2, b2))

ふたたび InvariantMonoidal

準備ができたのでInvariantMonoidalを実装します。

// semigroupに対するinvariant monoidal
given invariantMonoidalSemigroup: InvariantMonoidal[Semigroup] =
  new InvariantMonoidal[Semigroup]:
    override def unit: Semigroup[Unit] = semigroup((_, _) => ())
    override def product[A, B](
        fa: Semigroup[A],
        fb: Semigroup[B]
    ): Semigroup[(A, B)] = summon[Semigroupal[Semigroup]].product(fa, fb)

    override def imap[A, B](fa: Semigroup[A])(f: A => B)(g: B => A): Semigroup[B] = summon[Invariant[Semigroup]].imap(fa)(f)(g)

これを使うと既にSemigroupを定義済みの型からなるcase classに対してSemigroupを導けます。

final case class Foo(long: Long, date: Date)
object Foo:
  def unapply(foo: Foo): Option[(Long, Date)] = Some(foo.long, foo.date)

//invariant monoidal を使ってSemigroupを導く
val semigroupFoo: Semigroup[Foo] =
  invariantMonoidalSemigroup.imap(
    invariantMonoidalSemigroup.product(summon[Semigroup[Long]], summon[Semigroup[Date]])
  )(Foo.apply.tupled)(Foo.unapply.unlift)

別の見方をするとInvariantMonoidalはInvariantの一般系といえます。imap2...imap N-1を使ってimap3...imap Nを導けます。

extension [F[_]](im: InvariantMonoidal[F])
  def imap2[A, B, C](f: ((A, B)) => C)(g: C => (A, B))(fa: F[A], fb: F[B]): F[C] =
   im.imap(im.product(fa, fb))(f)(g)

  def imap3[A, B, C, D](f: ((A, B, C)) => D)(g: D => (A, B, C))(fa: F[A], fb: F[B], fc:F[C]):F[D] =
    imap2[A, (B, C), D]
      (f compose { case (a, (b, c))=> (a,b,c)})
      (g andThen { case (a, b, c) => (a, (b, c))})
      (fa, imap2[B, C, (B, C)](identity)(identity)(fb, fc))

まとめ

  • InvariantMonoidalはInvariantでありSemigroupalである
  • InvariantMonoidalはInvariantの一般系ともみなせる