6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Cats の K がつくクラスたち

Last updated at Posted at 2019-03-13

Cats には末尾が K で終わるクラスがいくつかある。通常の型に対する性質や操作を、F[_] のような型に当てはめたもので、頻繁に使うものではないが何度か見かけるうちにだんだん気になってくる。

今回は、SemigroupKMonoidKEitherKInjectKTuple2KFunctionK について書いてみる。(Cats は 1.6.0を使った)

SemigroupK と MonoidK

SemigroupKMonoidK は、その名前が示唆するように、おなじみの SemigroupMonoid にそれぞれ対応する。

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 が何であろうと問われないのが特徴になる。

インスタンスは ListChain などが提供されている。たとえば 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

FG が関手で、さらにその他もろもろの条件を満たせば自然変換になるが、特に関手でなくてはならないという制約はない。例えば下記のような型にも適用することもできる。

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) }

SemigroupSemigroupK、あるいは MonoidKMonoidK と同様の関係が、Function1FunctionK との間にも成り立っていて、andThencompose を使って合成することができる。

(f2g andThen g2h)(Ftype(100)) // Htype(100)
(g2h compose f2g)(Ftype(101)) // Htype(101)

さらに、上で説明した、EitherKTuple2K に関連して、orand も提供されていて下記のように使える。

// 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 というものが提供されている。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?