こんな記事を読んだ。
この記事の中に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
もおそらくどこにでも登場する概念だと考えておくとよさそう。ありがたいことに、便利な特性が用意されている。