Cats には末尾が K で終わるクラスがいくつかある。通常の型に対する性質や操作を、F[_]
のような型に当てはめたもので、頻繁に使うものではないが何度か見かけるうちにだんだん気になってくる。
今回は、SemigroupK
、MonoidK
、EitherK
、InjectK
、Tuple2K
、FunctionK
について書いてみる。(Cats は 1.6.0を使った)
SemigroupK と MonoidK
SemigroupK
と MonoidK
は、その名前が示唆するように、おなじみの Semigroup
と Monoid
にそれぞれ対応する。
K なし | K あり |
---|---|
Semigroup[A] は A の二項演算 combineをもつ | SmigroupK[F] は F[A] の二項演算 combineK[A] を持つ。 |
Monoid[A] は Semigroup[A] を継承する。 | MonoidK[F] は SemigroupK[F]を継承する。 |
Monoid[A] は 単位元 empty:A を持つ。 | MonoidK[F] は 単位元 empty[A]: F[A]を持つ。 |
K がつく方では F[A]
の A
が何であろうと問われないのが特徴になる。
インスタンスは List
や Chain
などが提供されている。たとえば Chain だと、以下のように書ける。
val mk = implicitly[MonoidK[Chain]]
// <+> は combineK の エイリアス
val ch1: Chain[Symbol] = Chain('a, 'b) <+> mk.empty <+> Chain('c)
// ch1: cats.data.Chain[Symbol] = Chain('a, 'b, 'c)
compose を使うと MonoidK[F]
に G[?]
を合成して、Monoid[F[G[?]]]
を作る事ができる。例えば MonoidK[Chain]
と IO
を合成すると以下のようになる。
val composed: MonoidK[λ[α => Chain[IO[α]]]] = mk.compose[IO]
val printIO: Any => IO[Unit] = o => IO { println(o) }
val ch2: Chain[IO[Unit]] = ch1 map printIO
// 明示的に composed の combineK を使う書き方
val ch3: Chain[IO[Unit]] = composed.combineK(ch2, one(printIO("hello")))
// 実は、明示的に combineK しなくても <+> が使える
val ch4: Chain[IO[Unit]] = ch3 <+> one(printIO(100))
ch4 を実行すると以下のようになる。
val io: IO[Chain[Unit]] = ch4.sequence
val result = io.unsafeRunSync() // ここで初めて println が評価される
// 'a
// 'b
// 'c
// hello
// 100
// result = Chain((), (), (), (), ())
EitherK と InjectK
どちらも 'Functional Pearl: Data types a la carte (以下、アラカルト論文)' で紹介されてたもの。
EitherK
は アラカルト論文では下記のような形で定義されていた。
data (f :+: g) e = Inl(f e) | Inr(g e)
Cats の エンコーディングでは以下のようになっている
final case class EitherK[F[_], G[_], A](run: Either[F[A], G[A]]) {
...
例えば以下のような、_[A]
の形の型があるとき
case class Ftype[A](a: A)
case class Gtype[A](a: A)
case class Htype[A](a: A)
sealed trait Nil[A]
Ftype :+: Gtype :+: Htype :+: Nil
は以下のように定義できる。
type FGH[A] = EitherK[Ftype, EitherK[Gtype, EitherK[Htype, Nil, ?], ?], A]
次のようにインスタンスを作る。
val yf: FGH[String] = EitherK.leftc(Ftype("a"))
// EitherK(Left(Ftype(a)))
val yg: FGH[String] = EitherK.rightc(EitherK.leftc(Gtype("a")))
// EitherK(Right(EitherK(Left(Gtype(a)))))
val yh: FGH[String] = EitherK.rightc(EitherK.rightc(EitherK.leftc(Htype("a"))))
// EitherK(Right(EitherK(Right(EitherK(Left(Htype(a)))))))
この調子だと右に行くほど面倒になるが、InjectK
を使えば少し簡潔に書けるようになる。
val zf: FGH[String] = InjectK[Ftype, FGH].inj(Ftype("a"))
//EitherK(Left(Ftype(a)))
val zg: FGH[String] = InjectK[Gtype, FGH].inj(Gtype("a"))
// EitherK(Right(EitherK(Left(Gtype(a)))))
val zh: FGH[String] = InjectK[Htype, FGH].inj(Htype("a"))
// EitherK(Right(EitherK(Right(EitherK(Left(Htype(a)))))))
FGH[A]
から、特定の_[A]
を取り出す操作も InjectK#prj
でできる。
InjectK[Htype, FGH].prj(yh) // 型が合っている場合
// Some(Htype(a))
InjectK[Ftype, FGH].prj(yh) // 型が違っている場合
// None
Cats では Free Monad と共に使われる。また例えば、Eff なんかを自前で実装しようと思ったときにもたぶん役立つ(はず)。
Tuple2K
上の EitherK
が OR だとすると、AND にあたるのが、この Tuple2K
。
ドキュメントによれば、論文 'The Essence of the Iterator Pattern' で紹介されたものだというが、Tuple2K
自体は以下のように簡単なもの。
case class Tuple2K[F[_], G[_], A](first: F[A], second: G[A]) {
...
たとえば次のようにインスタンスが作れる。
val t1: Tuple2K[Option, List, String] = Tuple2K("0".some, List("1", "2"))
Cats では各種の型クラスのインスタンスが提供されていて、例えば Traverse
なら以下のように使える。
val t = implicitly[Traverse[λ[α => Tuple2K[Option, List, α]]]]
val r1: Try[Tuple2K[Option, List, Int]] = t.traverse(t1)(s => Try { s.toInt })
// r1 = Success(Tuple2K(Some(0),List(1, 2)))
val t2: Tuple2K[Option, List, String] = Tuple2K("0".some, List("1", "a"))
val r2: Try[Tuple2K[Option, List, Int]] = t.traverse(t2)(s => Try { s.toInt })
// r2 = Failure(java.lang.NumberFormatException: For input string: "a")
FunctionK
F[_]
から G[_]
への変換をあらわすクラス。F ~> G
と書くことができる。
API ドキュメンテーションに載っているのは、List
から Option
への変換で下のように使える。
val listToOption: List ~> Option = λ[List ~> Option](_.headOption)
listToOption.apply(List(1, 2, 3)) // Some(1)
listToOption.apply(List()) // None
F
と G
が関手で、さらにその他もろもろの条件を満たせば自然変換になるが、特に関手でなくてはならないという制約はない。例えば下記のような型にも適用することもできる。
case class Ftype[A](a: A)
case class Gtype[A](a: A)
case class Htype[A](a: A)
これらの型の間の FunctionK
を以下のように用意しておいた上で、その他の演算を試してみる。
val f2g: Ftype ~> Gtype = λ[Ftype ~> Gtype] { case Ftype(a) => Gtype(a) }
val f2h: Ftype ~> Htype = λ[Ftype ~> Htype] { case Ftype(a) => Htype(a) }
val g2h: Gtype ~> Htype = λ[Gtype ~> Htype] { case Gtype(a) => Htype(a) }
Semigroup
と SemigroupK
、あるいは MonoidK
と MonoidK
と同様の関係が、Function1
と FunctionK
との間にも成り立っていて、andThen
や compose
を使って合成することができる。
(f2g andThen g2h)(Ftype(100)) // Htype(100)
(g2h compose f2g)(Ftype(101)) // Htype(101)
さらに、上で説明した、EitherK
と Tuple2K
に関連して、or
と and
も提供されていて下記のように使える。
// or
(f2h or g2h)(EitherK.leftc(Ftype(102))) // Htype(102)
(f2h or g2h)(EitherK.rightc(Gtype(103))) // Htype(103)
// and
(f2g and f2h)(Ftype(104)) // Tuple2K(Gtype(104), Htype(104))
補足
以前の記事に書いた Iota というライブラリで、入れ子になった EitherK をより簡単に使うための CopK
というものが提供されている。