Haskellのモナドについて

 とうとうHaskellの鬼門「モナド」の話です。色々と試してみました。基本の文法も分かってないのにモナドなんてやってる場合かよっていう状態ですが…

モナド

 私なりに理解したHaskellモナドとは、

  • モナドとは単なる入れ物(ただし、return関数と">>="演算子は必須)
  • MayBeモナドやIOモナドやStateモナドなど色々あるけど、同じ入れ物に入れると同じように扱えて便利だよねってだけ

 オブジェクト指向的に言えば、同じクラスから派生しておけば同じように扱えるよねってこと。
 これだけです。

圏論との関係

 そして「圏論」とHaskellの「モナド」の関係が難しくって色々と調べたんですが、結局正確なところはわかりませんでした。これも私なりの理解ですが、

  • モナドに入れたり出したり順番を入れ替えたりしても値は変わらないよとかの規則が圏論から来ている
  • モナド則も圏論から導かれる

 という程度なのではないかと思っています。とりあえず今は分からなくても良いんじゃないかなぁと。

モナド

 そして「モナド則」。これはちょっとは理解できました。

  1. (return x) >>= f == f x
  2. m >>= return == m
  3. (m >>= f) >>= g == m >>= (\x -> f x >>= g)

先にここに出てくる関数や演算子の型ですが

  • return :: Monad m => a -> m a
  • (>>=) :: Monad m => m a -> (a -> m b) -> m b

 returnはある値をモナドに入れます。
 たとえば、

return "Hell" :: [String]
["Hell"]
 リスト(リストもモナド)のreturn関数によって、文字列がリストに入りました。ちなみに":: [String]"は戻り値の型は文字列のリストですよと教えてあげています。そうしないと型推論もできないので、リストのreturnが呼べません。

 >>=(バインドと呼ばれるらしい)は左辺にm a右辺に"aを引数取りにm bを返す関数"を受け取ってm bを返す演算子です。(>>=)と括弧で囲むと関数としても使えます。具体的には、

[1..3] >>= \x -> [x * 2]
[2,4,6]
 [1..3]がm a、すなわちリストに入った1から3までの整数です。"\x -> [x * 2]"はラムダ式でxを受け取ってリストに入ったx * 2を返す関数、すなわち"aを引数取りにm bを返す関数"です。

 それではモナド則です。
 1は、「returnによってモナドに入れられたxに>>=演算子でfを適用したらf xだよ」です。やってみると、

return 2 :: [Int]
[2]
ですね。これに>>=演算子を適用してみると、
(return 2 :: [Int]) >>= (\x -> [x * 3])
[6]
です。これは、"(\x -> [x * 3])"に2を適用したものと同じだと言っています。やってみます。
(\x -> [x * 3]) 2
[6]
ですね。確かに同じです。

 2は、「モナドのデータコンストラクタmに>>=とreturnを適用したらデータコンストラクタmだよ」です。やってみると、

(1 : []) >>= return
[1]
です。これと"1 : []"が等しいと言っています。やってみます。
1 : []
[1]
です。正しいですね。
 (:)はリストのデータコンストラクタです。consと呼ばれるらしいです。
(:) :: a -> [a] -> [a]
 第一引数の値を第二引数のリストの先頭に付け足したリストを返します。
'c' : "def" → "cdef"
9 : [8, 7..0] → [9,8,7,6,5,4,3,2,1,0]

 3は、難しいですね…長いです。日本語で言うのも難しいんで実際に試してみます。
ちょっと演算子を関数に書き換えます。

  • (>>=) ( (>>=) m f) g == (>>=) m (\x -> (f x >>= g) )

これで合ってるかなぁ。Haskell演算子の優先順位と結合が全然わからんす。とりあえずやってみましょう。まずは左辺です。

(>>=) ( (>>=) (2 : []) (\x -> [x * 2]) ) (\x -> [x * 3])
[12]
です。次に右辺です。
(>>=) (2 : []) (\x -> (\y -> [y * 2]) x >>= (\z -> [z * 3]))
[12]
です。合っているようです。でもこんなの読めません…

 実はモナド則もよく分かってませんが、これを守ってないとreturnや">>="が思った通りの動きをしなくなるからねって言う程度のものだと思っています。

※ 上では実際に値を入れて評価した結果が同じことを確かめましたが、カリー化と関数の合成を駆使すれば同じ形に持って行けて証明になるのかな…

結局モナドってなんなの?

 結局、こう理解しました。「モナドとは、アスペクト指向である。」
 ">>="演算子で数珠つなぎになった関数の間になんらかの共通で利用できる仕組みを定義するもの。

 Maybeモナドは、前の関数が失敗しない限り後の関数を呼ぶ">>="演算子を持ちます。
 IOモナドは、RealWorldを引数と戻り値に持つことを強制し、参照透過性を守り遅延評価を防ぐ">>="演算子を持ちます。

 同様、通常のアスペクト指向のように関数の初めと終わりにログを出したり、トランザクションを開始・終了したりといったことも可能でしょう。(実装した訳じゃないのでわかりませんが…)

 今回はこれまでです。次回は実際にモナドを作れたらいいなぁと思っています。