2012年8月8日水曜日

クラウド温泉3.0 (16) / Kleisli

クラウド温泉3.0@小樽のセッション「Monadicプログラミング・マニアックス」で使用するスライドのネタ検討その16です。

関数型プログラミングでは、引数が1つの関数が非常に重要です。極端に言うと引数が1つの関数を合成して、プログラムを構築していきます。

もちろん引数が2以上の関数も使いますが、この場合はカリー化や部分適用を使って引数が1つの関数に持っていくのが重要なテクニックになっています。

引数の数が1の関数

引数の数が1つの関数として、3倍する関数mul3と5を加える関数plus5を定義します。

val mul3 = (_: Int) * 3
val plus5 = (_: Int) + 5

引数1の関数は以下のようにmapメソッドといった高階関数に指定して処理を合成することができます。多くの高階関数は引数の数が1の関数を入力にするので、引数の数が1の関数は再利用可能な部品として利用価値が高くなります。

scala> List(1, 2, 3).map(mul3)
res19: List[Int] = List(3, 6, 9)

A→M[B]

FunctorのmapメソッドはA→Bという形の関数を引数に取りますが、MonadのflatMapはA→M[B]という形の関数を引数に取ります。MはMonadなので、AからBをMonadでくるんだものを返す関数ということになります。A→M[B]も引数の数が1の関数の一種ですが、Monadを返すところが特殊化されています。

Monadは関数型プログラミングに非常に重要な構成要素です。つまり、A→M[B]の関数も再利用可能な部品として利用価値が高いと考えられます。

先ほど定義したmul3とplusをMをListとしてA→M[B]化した関数mul3lとplus5lは以下になります。狙いが分かりやすいようにmul3lは0以上の値、plus5lは偶数を有効として扱うように機能追加しました。

val mul3l = (x: Int) => if (x >= 0) List(x * 3) else Nil
val plus5l = (x: Int) => if (x % 2 == 0) List(x + 5) else Nil

mul3lをListのmapメソッドに適用すると以下のようになります。

scala> List(1, -2, 3, 4).map(mul3l)
res33: List[List[Int]] = List(List(3), List(), List(9), List(12))

さて、mul3lをListのflatMapメソッドに適用すると以下になります。-2に対応する要素が結果から取り除かれていますが、これがモナドの効果です。

scala> List(1, -2, 3, 4).flatMap(mul3l)
res34: List[Int] = List(3, 9, 12)

mul3lとplus5lを連続してflatMapで適用すると以下になります。

scala> List(1, -2, 3, 4).flatMap(mul3l).flatMap(plus5l)
res32: List[Int] = List(17)

Kleisli

mul3lとplus5lの組み合わせが頻出する場合、これを一つの関数に合成しておくと便利です。ただし、mul3lやplus5lは引数と返却値の型が違うので単純な関数合成はできません。

scala> mul3l andThen plus5l
<console>:16: error: type mismatch;
 found   : Int => List[Int]
 required: List[Int] => ?
              mul3l andThen plus5l
                            ^

そこで登場するのがKleisliです。

"プログラムとはクライスリ圏の射である(program is arrow of Kleisli category)"という背景があり、クライスリ圏の射に対する演算をプログラミング言語で扱うメカニズムがScalaやHaskllのモナドということになるかと思います。

そして、クライスリ圏の射はScalaではA→M[B]の関数が対応します。A→M[B]の関数はMonadのflatMapメソッドに適用できますが、単体で扱いたい場合には特別なサポートがあると便利です。このような目的で使用するのがScalazのKleisli(以下単にKleisliと呼びます)です。

KleisliはA→M[B]の関数をくるんで、クライスリ圏の射として操作するためのメカニズムを提供します。

mul3lとplus5lをKleisli化すると以下になります。mul3lkとplus5lkがKleisli化したmul3lとplus5lです。

scala> val mul3lk = kleisli(mul3l)
mul3lk: scalaz.Kleisli[List,Int,Int] = scalaz.Kleislis$$anon$1@6119d27b

scala> val plus5lk = kleisli(plus5l)
plus5lk: scalaz.Kleisli[List,Int,Int] = scalaz.Kleislis$$anon$1@38ab3c51

mul3lkとplus5lkはKleisliなので演算子>=>で合成することができます。

scala> val m3p5lk = mul3lk >=> plus5lk
m3p5lk: scalaz.Kleisli[List,Int,Int] = scalaz.Kleislis$$anon$1@76f4a74b

scala> m3p5lk(1)
res40: List[Int] = List()

scala> m3p5lk(4)
res41: List[Int] = List(17)

合成したKleisliであるm3p5lkをflatMapに適用すると以下のようになります。このm3p5lkのようにA→M[B]をKleisli化して合成することでMonad向けの部品を整備することができます。

scala> List(1, -2, 3, 4).flatMap(m3p5lk)
res39: List[Int] = List(17)

なおKleisliは引数1の関数でもあるのでmapメソッドに適用することもできます。

scala> List(1, -2, 3, 4).map(m3p5lk)
res43: List[List[Int]] = List(List(), List(), List(), List(17))

パイプライン・プログラミング

Kleisliをパイプライン・プログラミングの観点でみてみましょう。

まずListからmapやflatMapメソッドを適用する形も処理が左から右に流れていくのでパイプライン・プログラミングといえます。これがMonadを使ったパイプライン・プログラミングです。

scala> List(1, -2, 3, 4).flatMap(mul3lk).flatMap(plus5lk)
res46: List[Int] = List(17)

さらに関数の合成としてのパイプライン・プログラミングとしてはパイプライン演算子とKleisliの合成を使って以下のような処理が可能です。

scala> 4 |> mul3lk >=> plus5lk
res45: List[Int] = List(17)

0 件のコメント:

コメントを投稿