モナドのKleisli圏

圏論からHaskellのIOモナドへの最短距離の近道を示してくれる文書を見つけた。

モナドへの近道・Haskell からの寄道』 中村翔吾著

がそれだ。数学的にきちんと説明してあるので、読んですぐ理解できるようなものではないが、何となくIOモナドの考え方の雰囲気のようなものは伝わった気がする。

大げさな話になるが、この世界は何でできているかというと、いろいろな物とそれらのあいだの関係で成り立っていると言ってもいい。すなわち、世界のモデルの雛形として、集合Xと集合YとX->Yの関数 f(x) の集まりである関数の集合 Hom(X,Y) を考えることができるということだ。

たとえば、集合 X={1, 2} と集合 Y={a, b} からなる世界があり、X->Yの関数を集めた集合、Hom(X,Y) ={f, g} があったとする。すると、X, Y, Hom(X,Y) の三つの組みでこの世界は成り立っていると言えるだろう。これを圏 C と呼ぶことにする。ところが、また、別のところに、集合V={3, 4}と集合W={v, w}とそれらの関数Hom(V,W)={j, k}からなる圏 D という世界があったとする。

当然興味が湧くのは、そのような圏 C と圏 D の構造が同じものなのか、違うものなのかということだ。これをどうやって調べるのかというと、圏Cの要素から圏Dの要素への写像Fを作ってみればいいのだ。Cの世界の集合XはF(X)によって、集合Vに写像する。また集合YはF(Y)によって、集合Wに、Hom(X,Y) はF(Hom(X,Y))によって、Hom(V,W)に写像してやる。このような写像Fが存在すれば、圏Cと圏Dの間には構造的な類似があることがわかる。このFのことを圏論では関手と呼ぶ。

ところで、Hom(X,Y)の要素、f g にはその合成 f * g を定義できることを前回のエントリーで示した。また id * f = f * id = f となるような単位元の関数 id も存在することを示した。実は関手 F はF(idx) = idy, F(g*f) = F(g) * F(f) のように、単位元と関数の積の関係が保存されている必要がある。IOモナドで、return 関数が単位元で、bind 演算子が積にあたる。

要するに、関数の単位元や合成といった性質を保存しながら、圏Cの世界を圏Dの世界へ映しだすのが関手Fの機能だ。

上の例では関手Fは圏Cを圏D上に写像していたが、逆方向の圏Dから圏Cへの関手Gも考えることができる。この際に関手Fと関手Gの合成写像GFは圏Cから圏Cへの写像となる。したがって圏Cの対象cとGFの像GFcとの間の関数ηを考えることができる。同様に圏GFの像と圏Gの対象gとの間の関数εを考えることができるが、これによって、

F = εFη = F や G = ηGε = G

のようにGやFの前後に適用することによって関手の恒等関数としてはたらくことのできる自然変換、ηとεを取ることができる。この、関手F,関手G、η、εの4つを組みにして、随伴<F, G, η, ε>という。

これまでは、圏Cと圏Dの間の関手を考えてきたが、圏Cから圏Cへの関手Tも考えることができる。この場合は圏Cの自分自身の姿を、自分自身の中に映し出すことになる。合わせ鏡のようなものだ。このような関手Tでは随伴は<T, η, μ>となり、これをモナドと呼ぶ。関数ηは単位元、関数μは乗法である。

モナドの関手や単位元や乗法のほんとうの意味はわからなかったが、モナドという名前が指しているのが、圏でも、圏の関数でもなく、関手と単位元と乗法の三組をさすものだということがわかったのは収穫だった。

このモナドが決まると、このモナドから派生するKleisli圏というのが決まる。このKleisli圏の特徴が次のようなものらしい。

• XT の対象はX の対象である,
• 射の集合XT (xT , yT ) はX(x, Ty),
• XT における対象xT の恒等射は,ηx : x → Tx,
• fT ∈ XT (xT , yT ), gT ∈ XT (yT , zT ) の合成はμz ◦ TgT ◦ fT : x → Ty → T2y → Tz.

これをよく見ると、IOモナドに出てくる馴染みのある表現が見られる。例えば、Kleisli圏の射は引数が x で戻値が Ty つまり IO a だ。また、恒等射 nx は return :: x -> IO x だし、合成の型は a -> IO b -> b -> IO c というふうに写像されていく。IOモナドの単位元(return) とbind演算子(>>=) はKleisli圏の性質そのものだったのだ。

上の定義でIOモナドの関手Tと単位元とbind演算子は既定のものだから、プログラマが操作するのは、個々の射(関数)だけだ。そうして、個々の射はX(x, Ty)の要素なので、「引数一つ、戻値IO」という規定を満たしてさえいればいい。

詳しい数学的な意味はわからなかったが、結局モナドとは、Haskellのいろいろな型のデータとそれらに関連する関数からなる圏Cの、それ自身に対する関手(圏から圏への写像)Tと単位元、合成の三組を指すことがわかった。また、その三組によってKleisli圏が定められる。Kleisli圏の射(関数)は「引数ひとつ、戻値IO型」の関数で、これらを合成することによって様々なKlieisli圏の関数を作ることができるが、そうして出来上がった関数がHaskellのIOモナド関連のプログラムだ。

結局、IOモナドとはHaskellの世界をIOモナドのKleisli圏という世界にIOモナドの関手によって映写して、副作用による影響をIOモナドの世界に閉じ込めてしまう方法だったのだ。おまけにその方法はあっけないほど簡単だ。一変数のHaskell世界の関数を return 関数でIOモナドのKleisli圏の関数に変えてやるだけだ。

IOモナドを関数型に埋め込まれた手続き型のプログラムととらえると、意味の分からないエラーがでて困惑するが。IOモナドのプログラムは結局IOモナドのKleisli圏の射(関数)を>>=演算子で組み合わせたものだと考えると、プログラムの発想がずいぶん楽になるのではないだろうか。それも、特殊な発想ではなく、Unixのパイプライン処理で経験済みの方法だ。前段の関数のデータを後段のフィルターにつないでやるというあの感覚だ。

「引数ひとつ、戻値IO型」というルールはどうも理論的にも根本的なようだ。理論がどうあれ、この「引数ひとつ、戻値IO型」のルールでIOモナドの自前の関数をつくれば、IOモナドを自在に活用できるようになるだろう。
[PR]
by tnomura9 | 2010-10-26 08:41 | Haskell | Comments(0)
<< IOモナドでパイプラインプログラム 圏論とIOモナド >>