Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

モナドで悟りをひらきたいのなら - 図でわかる(?)モナド

圏論の最大の武器はダイアグラムなので、モナドで悟りをひらきたいのならダイアグラムを使えばいいんじゃないでしょうか。

ダイアグラムの書き方

例えば、「 f :: a -> b 」とか「length :: [a] -> Int」は以下のように書きます。型を点で、関数を矢印で書きます。


ダイアグラムの利点は、fやlengthの中身を忘れて簡略化することができることです。人間の脳ができることには限りがあるので、注目する情報が少ない方が理解しやすくなるってスンポーです。

なお、 合成 g . f は図示する時に順が逆になるので気をつけて下さい。これは、合成関数の適用が g ( f x ) と書けることに由来してます。まずfを適用し、次にgを適用するということです。


return と >>= の図示

今回のダイアグラムの約束として、元となる型(Bool, Char, Int 等)は最下段に書きます。そして、それらに対応するモナドの型(Maybe Bool, Maybe Char, Maybe Int等)をそれぞれの真上に書きます。

return :: a -> m a を図示すると、以下のようになるでしょう。aだけじゃなく、bやc、任意の元になる型から上向きに出ている矢印がreturnです。


また、 (>>=) :: m a -> (a -> m b) -> m b は、f :: a -> mb のような関数を (>>= f) :: m a -> m b へ変換する関数と見れるので、以下の点線のように表せます。ちょうど矢印の左端を持ち上げるイメージです。


なお、(>>=)は2項演算子なので、 m >>= f と (>>= f) m が同じであることに気をつけて下さい。以下では、双方の書式を使います。

モナド

(return x) >>= f == f x の図は以下。型に含まれる値は、青字で書いてます。この図では、xはa型の値です。この図を、上、右と矢印を辿ったのと、いきなり斜めの矢印を辿ったときの結果が等しい*1と言うのが、この式の意味です。

要素xを忘れて図の矢印だけから考えると、この式は f == (>>= f) . return と言っても同じことがわかります。


次は、 m >>= return == m の図。さっきの図ではベースの型を a と b としましたが、 a の型を2つ並べて図示すると、 return は斜めに入る矢印なので、 (>>= return) を考えることができることがわかります。そして、idは恒等関数です。ここでも要素mを忘れれば、 id == (>>= return) と言ってもいいことがわかります。


最後は (m >>= f) >>= g == m >>= (\x -> f x >>= g) です。まず、左辺の図示は以下。


右辺の方は (\x -> f x >>= g) は (>>= g) . f と書けるので、以下のようになります。


2枚の図で、m a から m c へ行く道が二通りできたと思いますが、この二つが等しい(可換)であることが条件となります。これも要素mを忘れれば、 (>>= g) . (>>= f) == (>>= ( (>>= g).f )) と書けることが見えてきます。

fmap と returnの性質

fmapはFunctorクラスの関数で、地べたを張っている関数をモナドのある上段へ平行に引き上げます。なお、 (>>=) は斜めの矢印の片側を引き上げるだけでした。

このfmapは、実はモナドの関数から作れます。以下のように、fとreturnを合成して斜めの矢印を作ってから、 >>= で引っ張り上げれば、上の世界に行けます。

底辺の様々な型についてこのfmapとreturnを考えると、以下のようなハシゴ状の構造を考えることができます。


そして、これらは全てどの順で矢印を辿っても同じ結果になることが証明できます*2*3

これは、底辺の演算を先にやって最後に上に持ち上げるのと、先に上に持ち上げた世界で演算した結果とが等しいということです。言い換えると、モナドはベースとなる世界の演算の結果を全て保存しますモナドは、ベースとなる世界の値の意味を守りつつ、さらに新たな意味を付け足すものと言えます。

ケーススタディ

以下のような簡単なHaskellのプログラムを考えます。

module Main where

main :: IO ()
main = getLine >>= putStrLn . reverse

登場人物を図示すると、以下です。Haskellのプログラムでは、 IO () の値を一つ作り、mainと言う名前でバインドするということを思い出しましょう。 IO () は 「() という型を拡張し、この型が持つ値(ないけど)の背後に一連のアクションも含んでいる型」の値です。


結合とかを全部書くと、以下です。うまくつながって、 IO () 型の値を一つ定義していることが読み取れるでしょう。


10/31 追記 射と対応 (おまけなので圏論に興味がある方だけ)

これらの図で、returnやfなどの関数を矢印で書きましたが、>>= や fmap は点線のような操作として表しました。しかし実は、Haskell上では >>= も fmap も関数なので、実線の矢印でダイアグラムに登場させることもできます。ただし、この場合には実線の矢印としてダイアグラムに登場させてもあまり意味がありません。それは、圏論的には return や f がであるのに対し、 >>= や fmap は射や対象の間の対応(写像)だからです。

Haskellにおいては、射も対応も関数で表現されますので、これらの区別はつきません。Haskellを圏として見る時にはこれらを意識して区別をしないと、ダイアグラムを書く時に、書いてみたのだけどなんのことやらということになります。Haskellにおいて、対象(object)は型となります。ダイアグラムを書くときは、射と対象と見なせるものを矢印と点で書くようにします。

*1:可換であると言います。

*2: 具体的には、 fmap g . return = return . g

*3:この性質を、returnは自然変換である、と言います。もうちょっと言えば、returnは恒等関手から対象関数mと射関数fmapによる関手の間の自然変換です。