以下のエントリ
「関数型Ruby」という病(6) - 関数合成と文脈、Proc#liftとProc#>=、そしてモナ
に便乗して、(一部の例を)Scalaに翻訳してみるのと、ScalazのKleisliの話をします。Scalaや関数型に興味のない人は読まないほうがいいかもしれません。わざとMonadとかFunctorの用語も出します。また、もとのエントリの良し悪しとか、Rubyでああいうことをやることの是非などはあまり話しません、というか、それが主目的ではありません。とにかく便乗してScalaの話するのと、あえて元記事では避けている「関数型の用語を出した説明」を少しします。
まず、(nilはOptionを使うということにして)もとの>=の例を直訳すると以下
val f = (_: List[Int]).headOption val g = (_: Int) + 1 val h = (_: Int) * 2 val i = f.andThen(_.map(g).map(h)) println(i(List(3, 5)))
合成をする際に、一箇所だけ
_.map(g).map(h)
というところで無名関数が出現してしまっています。しかし、実用上この標準ライブラリの範囲のScalaのコードで十分でしょう。
しかし、あえてScalazのKleisliを登場させて、完全なポイントフリーに書き換えてみましょう。
val f = (_: List[Int]).headOption val g = (_: Int) + 1 val h = (_: Int) * 2 import scalaz._, std.option._ val i = Kleisli(f).map(g).map(h) println(i(List(3, 5)))
Kleisliというのは、一言で言うと関数のラッパーです。しかしもう少し細かくいうと A => F[B] と、関数の戻り値型が少し違っていて、具体的には Int => Option[String] とか String => List[String] とかのラッパーです。
戻り値の部分にでてくるFというのは、なんらかの型パラメータを1つ取る型です。
今回の場合、fは List[Int] => Option[Int] なので、Kleisli[Option, List[Int], Int]です。
さて
Kleisli(f).map(g).map(h)
だけだと、モナドまったく関係ありません。関数自体はFunctorですが
- A => F[B] という関数において、FがFunctorならば、A => F[B] も*1Functorになる
ということを言っているに過ぎません。
しかし、yuroyoroさんのブログにでてくる例が単純すぎる*2だけで、lambda_driverの>=というのは、Kleisliの合成でしょう。
というわけで(?)なんか話の流れが強引ですが、合成するgやhがInt => Intではなく、Int => Option[Int] の場合にScalazで書くとどうなるか?という例が以下です。
val f = (_: List[Int]).headOption // 面倒なので内部処理省略。とにかくxもyも何か失敗するかもしれない処理 val x: Int => Option[Int] = ??? val y: Int => Option[Int] = ??? //もしScala標準ライブラリのみで書く場合 val z = f.andThen(_.flatMap(x).flatMap(y)) import scalaz._, std.option._ // ScalazのKleisli使って書く場合 val z = Kleisli(f) >==> x >==> y println(z(List(3, 5)))
https://github.com/scalaz/scalaz/blob/v7.1.0-M5/core/src/main/scala/scalaz/Kleisli.scala#L16
yuroyoroさんのblogにでてくる例は、後続のgやhが必ず成功する関数ですが、nilチェックはすべてにおいて走るはずなので、lambda_driverの>=というものは、上記のScalaコードと同等のこと*3が可能なはずです。
結局ScalazのKleisliや、lambda_driverの>=がなんなのか?を、わざと関数型の言葉を使って説明すると
yuroyoroさんは
実際、モナ……則どころかモナ……の形すらしていない(returnもbindもない)のでモナ……ではないのだが
といっています。たしかにreturnはないというか、Kleisliを合成するのに限ればそもそも必要ない*7のでそれ自体は問題ないですし、liftに渡すコンテキストのオブジェクトを適切に定義すれば、たぶんMaybeモナド以外のもの(たとえばListで、Kleisli[List, _, _])として使えるようにもなっていると思います。
追記。mentionもらった
https://github.com/yuroyoro/lambda_driver/blob/d33b609d/lib/lambda_driver/context.rb#L11
このようにlambda_driverの>=は、scalaz.Kleisliの>==>と大体同じものです。*8 *9
ということが言いたいだけでした。
*1:普通の関数としてのFunctorとは違う意味でも
*2:gやhも失敗する可能性がある処理でもよいはず
*3:つまり、最初のだけではなく、失敗するかもしれない複数の処理を連鎖して合成すること
*5: 先程も言ったが A => F[B] と B => C を合成するだけだったら、 UpStar https://github.com/scalaz/scalaz/blob/v7.1.0-M5/core/src/main/scala/scalaz/Profunctor.scala#L67 という物があるのだが、Haskellならともかく、Scalazだと型推論の関係で使いにくすぎる・・・
*6:lambda_driverの場合は、FがMonadでないパターンでも、使おうと思えば色々と使えてしまいますが
*7:そして、Monadからreturnを除いたものは、ScalazではBindと呼ばれる型クラスである。実際ScalazのKleisliのメソッドのほとんどが、MonadではなくBindを要求するようになっている
*8:もちろん、nilを利用する都合とかあるので、厳密なこと考えるとあばばばば、なので "大体同じ"という言い方にした
*9:あと、"標準出力に吐く"の例のほうは、関数合成の処理の合間に処理をはさみこんでいるだけで、たぶんMonad関係ないので、あの例をもとにMonadのことを考えるのはやめましょう。最初の"失敗するかもしれない"のほうは、ScalaでほぼKleisli[Option, _, _]の合成として表せるよ、という話です。