こんな記事を読んだ。
この記事の中にKleisliという概念が登場する。自分は圏論の専門家ではないのでKieisli圏の話ではなく、Catsを使ってKleisliを扱う方法、どう便利なのかについて勉強したことをまとめてみる。
flatMapの中身
Kleisliとは以下のような定義である:
Kleisli[F[_], A, B] = A => F[B]
そう、ただのラッパーである。この等式の右辺をよく見ると、モナドをflatMapするときに渡すアレのことだとわかるはずだ。
val f = (n: Int) => Some(n * 2) Some(42).flatMap(f) // ここのfの型がKleisli[Option[_], Int, Int]と等価なものになっている // => Some(84)
Catsでは、Kleisliを作るにはKleisliコンストラクタを使って関数を包む必要がある(たぶん、モナドの情報を型パラメータとして引っ張り上げてくるのに必要なのだ):
import cats._, cats.data._, cats.syntax.all._ val k = Kleisli(f) k(42) // => Some(84) // runで中身の関数を取り出せる Some(42).flatMap(k.run) // => Some(84)
これは当てずっぽうだけれど、おそらくflatMapするときに渡すあの関数自体を独立して扱えたら便利そうという発想で導入された概念だと思っておけばよさそう(実際は多分そんなことはなくて複雑な理論があるのだろう・・・)。
Kleisliは合成できる
Kleisli自体にはいくつかの望ましい特性がある。その一つが「F[_]がモナドであればKleisli[F[_], A, B]は合成できる」ということだ((より厳密には、F[_]はFlatMapである必要があるが、初心者はMonadだと考えておけばよい))。
val k = (s: String) => try { Some(s.toInt) } catch { case e: NumberFormatException => None } val l = (n: Int) => if (n % 2 == 0) Some(n*2) else None
このような2つのKleisliがあるとき・・・
import cats.implicits._ import cats._, cats.data._, cats.syntax.all._ val lk = Kleisli(l) compose Kleisli(k) lk("42") // => Some(84)
ちゃんと合成できている。この操作は、最初からSomeで包んだ値に対して2回flatMapするのと等価な操作だ:
Some("42").flatMap(k).flatMap(l) // => Some(84)
2回flatMapするようなとき、kとlを合成したくなるのは自然な欲求だと思う。
ちなみにcomposeの順序を替えたandThenも用意されている。
特殊な合成いろいろ
localメソッド
Kleisli[F[_], A, B]に対してAA => Aを渡すことで、Kleisli[F[_], AA, B]に変形する。つまりKleisliの入力だけ変形するメソッド。
import cats.data.Kleisli, cats.implicits._ type ParseResult[A] = Either[Throwable, A] val parseInt = Kleisli[ParseResult, String, Int](s => Either.catchNonFatal(s.toInt)) parseInt.local[List[String]](_.combineAll).run(List("1", "2")) // => ParseResult[Int] = Right(12)
lower メソッド
用途不明。結果をさらにpureしてくれる。
val k = (s: String) => try { Some(s.toInt) } catch { case e: NumberFormatException => None } val lower = Kleisli(k).lower lower("42") // => Some(Some(42)) lower("abc") // => Some(None)
liftメソッド
別のApplicativeの文脈に押し上げてくれるメソッド。例えばkをListに対して使えるようにしてくれる:
val listK = Kleisli(k).lift[List] List("42", "43", "abc", "def", "44").flatMap(li.run) // => List(Some(42), Some(43), None, None, Some(44))
便利なときには便利だと思うけどパッと用途は思いつかない。
Kleisliの型クラス
KleisliはF[_]がどの型クラスのインスタンスであるかによって様々な型クラスのインスタンスになることができる。
また、このドキュメントによると、Monoid[F[B]]のインスタンスがあれば、Monoid[Kleisli[F, A, B]]のインスタンスも得られるといっている。
例えば、数字に点数を付ける2つの関数があるとする:
val score1 = Kleisli { (n: Int) => if (n % 2 == 0) Some(1) else None } val score2 = Kleisli { (n: Int) => if (n >= 10) Some(1) else None }
それぞれ、偶数なら1点、10以上なら1点を与えるような関数だ。
Option[Int]はMonoidなので、Kleisli[Option, Int, Int]同士をcombineできるようになる:
val score = score1 |+| score2 score(0) // => Some(1) score(5) // => None score(42) // => Some(2)
いずれの点数も得られなかった数字にはNoneが返された。そして両方に合致している場合は加算の結果2が返されている。これはルールエンジンのようなものを作るときに役立ちそうだ。
まとめると
Kleisliとはm.flatMap(f)するときのfの部分の型を抜き出したものであるKleisliは型が合っていれば関数のように合成できるF[_]によっては、Kleisli自体も様々な型クラスのインスタンスとして振る舞う
Scalaを書いているとflatMapはどこにでも現われる概念なので、Kleisliもおそらくどこにでも登場する概念だと考えておくとよさそう。ありがたいことに、便利な特性が用意されている。