2012年3月15日木曜日

関数型とデータフロー(4)

要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』で使用するスライドについて背景説明を行っています。

今回は背景説明第12弾として、「関数型とデータフロー(4)」として用意した以下の図を説明します。

前回はモナドを結合してデータフローを実現する方法を説明しました。

これが一般的なモナドの使い方ですが、モナドを部品として使いまわそうとすると、若干問題が出てきます。

モナドの合成

前回使用したOptionモナドを使ったデータフローの配線です。再利用のためにp5m10という名前をつけました。

val p5m10 = (_: Int).some >>= plus5o >>= mul10o

以下のように動作します。

scala> 3 |> p5m10
res64: Option[Int] = Some(80)

ここで問題なのが、p5m10を合成して新しいデータフローを作ることが難しいことです。試してみると以下のようになりますが、エラーメッセージから分かるとおりp5m10の入力型と出力型が合っていないのが原因です。

scala> p5m10 >>> p5m10
<console>:17: error: type mismatch;
 found   : Int => Option[Int]
 required: Option[Int] => ?
              p5m10 >>> p5m10
                        ^

scala> p5m10 >>= p5m10
<console>:17: error: type mismatch;
 found   : Int => Option[Int]
 required: Option[Int] => Int => ?
              p5m10 >>= p5m10
                        ^

関数型とデータフロー(1)関数型とデータフロー(2)で説明したとおり、関数の場合は以下のように「>>>」などを用いて関数の合成を行うことで新しい関数を生成することができます。このことによって、関数をデータフローの部品として使用することが容易になります。

scala> val p5m10 = plus5 >>> mul10
p5m10: Int => Int = <function1>
scala> 3 |> p5m10
res0: Int = 80

モナドをデータフローの部品として使用することを考えると関数の合成と同様の使い勝手で利用できるモナドの合成が必要になってきます。

kleisli

モナドの合成で登場するのがクライスリ圏(kleisli category)です。クライスリ圏については以下のページが参考になりますが、理論的にはかなり難解でボクもきちんとは把握できていません。今の所、モナドを合成するにはクライスリ圏に持ち上げる、というように理解しています。

ただ、プログラミングのイディオムとしては慣れてしまえばそれほど難しくはありません。簡単に言うと、モナドを合成するためにKleisliという入れ物を用いる、ということです。プログラミングイディオムとしてのKleisliは以下のページが参考になります。

そこで、図に登場する以下の式です。

ScalazはKleisliのための一連の機能を提供しています。☆はモナドのbind演算に用いる関数をKleisliに入れる演算を行う関数です。

val plus5o = (a: Int) => (a != 5).option(a + 5)
val mul10o = (a: Int) => (a % 10 != 0).option(a * 10)
val plus5ok = ☆(plus5o)
val mul10ok = ☆(mul10o)

☆はkleisli関数の別名なので、以下のように書くこともできます。

val plus5ok = kleisli(plus5o)
val mul10ok = kleisli(mul10o)

☆(plus5o)によってKleisli化されたplus5oが、☆(mul10o)によってKleisli化されたmul10oが定義されます。それぞれにplus5ok、mul10okという名前をつけています。

関数plus5okとmul10okはそれぞれ以下のように動作します。Int型を引数にとってOption[Int]型を返します。

scala> 3 |> plus5ok
res3: Option[Int] = Some(8)

scala> 3 |> mul10ok
res4: Option[Int] = Some(30)

plus5okとmul10okは、いずれもKleisli化されている(Kleisliの容器に入っている)ので、以下のように演算子>=>で合成することができます。合成して作成した新しいモナド演算を行う関数にp5m10okという名前をつけます。

scala> val p5m10ok = plus5ok >=> mul10ok
p5m10ok: scalaz.Kleisli[Option,Int,Int] = scalaz.Kleislis$$anon$1@6a2a9a0e

以下のように期待通りに動作しました。

scala> 3 |> p5m10ok
res5: Option[Int] = Some(80)

Kleisli化されたOptionモナドの動作

Kleisli化されたOptionモナドを使ったデータフローの配線は以下のものになります。

plus5ok >=> mul10ok

Int型の値を受け取り、これをplus5ok関数、mul10ok関数で合成された関数に流していきます。概念的には、図にあるようにKleisliの箱の中にあるOptionの箱の中を左から右へデータが流れていきます。

以下ではそれぞれのパイプラインの動作についてみていきましょう。パイプライン内の動作は基本的に前回のものと同様です。

正常動作

データフローにSome(3)を流すと、データフローの計算は成功し、計算結果としてSome(80)が出力されます。

plus5oでエラー

データフローにSome(5)を流すと、データフローの計算は失敗し、計算結果としてNoneが出力されます。

5はplus5oでエラー判定されるため、plus5oから失敗側のパイプラインが動作します。

mul10oでエラー

データフローにSome(15)を流すと、データフローの計算は失敗し、計算結果としてNoneが出力されます。

15はplus5oでは正常な値なので、plus5oの結果は20となりますが、この20がmul10oでは10で割り切れるためエラー判定され、mul10oから失敗側のパイプラインが動作します。

ノート

「関数型とデータフロー」というタイトルで、関数とモナドを使ったデータフロー構築手法についてみてきました。

関数、モナドという軸と呼出し、合成という軸があります。それぞれの軸の組合せによる、関数/モナドの呼出しと合成の見取り図を表にしてみました。








関数/モナドの呼出しと合成
呼出し合成
関数g(f(x))f >>> g
モナドx.some >>= f >>= g☆(f) >=> ☆(g) |

簡単な応用であれば左側の「呼出し」で十分なのですが、データフロー的な利用方法を目指して部品化、再利用を考えると右側の「合成」が必要になってきます。

関数の合成は、ScalaでもcomposeメソッドやandThenメソッドとして実現されていますが、一連の記事ではScalazのArrowの方を利用してみました。(ScalazではArrowというメカニズムの上で関数合成を可能にしています。Arrowは圏論における射(arrow, morphism)の実現なので、適切に圏を設定すれば関数以外の射の合成にも適用できます。)

モナドの合成についてはScalaの標準機能にはないのでScalazのKleisliを利用する必要があります。

0 件のコメント:

コメントを投稿