関数型RubyとScalaとKleisli

以下のエントリ

「関数型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の>=がなんなのか?を、わざと関数型の言葉を使って説明すると

  • A => F[B]という関数があり、そしてFがMonadの場合*4に、 A => F[B] と B => C や B => F[C] を合成するためのもの

です。*5
*6


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:つまり、最初のだけではなく、失敗するかもしれない複数の処理を連鎖して合成すること

*4:もっと正確に言うと、MonadではなくBind

*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, _, _]の合成として表せるよ、という話です。