2012年12月20日木曜日

初心者のモナド

これは、 Haskell Advent Calendar 2012 20日目の記事です。

はじめに

Haskellで(趣味の)プログラムを書き始めて、ほぼ1年になりました。最初、モナドを調べたとき、さっぱり分からなくて困りましたが、今は少なくとも使うことはできるようになりました。初心者ですが、モナドの壁を振り返ります。

モナドを理解するときの壁

  • モナドが何かについて考えること
  • モナドの(>>=)は、何か分からない
  • モナド則が謎
今振り返ると、大雑把にいって、上の3つの壁がありました。

「モナドが何かについて考えること」による壁

1年前、「Haskell のIOは、モナドだ」というのを見て、モナドに興味を持ちました。自然の成り行きとして、「モナド」が何かを調べました。すると「モナド」は、圏論の用語だというところに行き着くわけですが、圏論はなかなか敷居が高くて理解することはできませんでした。(普通そうですよね!)

今は、少なくともHaskellのモナドを使うにあたり「モナド」が何か分からなくてもいいのではないかと思っています。(圏論を知っていれば、もっと別の理解ができるとは思いますが。)割り切って考えるのがいいと思います。Haskellにおいて、モナドが何かというと、それは、class Monadのインスタンスになっているデータ型である。それ以上のものでもそれ以下のものでもないと考えてみましょう。

これを認めることができれば、class Monadが何かを考えればいいことになり、とりあえず圏論は気にしなくてすみます。

「モナドの(>>=)は何か分からない」壁

先ほどの方針にしたがって、class Monadについて考えます。重要なところだけ抜き出すと、次のようになります。

  class Monad m where
    return :: b -> m b
    (>>=) :: m b -> (b -> m c) -> m c

これだけ見ても、さっぱり分かりません。returnは、まあ許すとして、(>>=)が何を意図しているのかがどうもよく分からなかった訳です自分で書いていたプログラムの一部が、これって foldl に置き換えられるじゃんと思えるようになった(foldlの方がfoldrより先に使えるようになった)ころ、(>>=)の意味に気づきました。

(>>=)は、
関数h:: b -> m cを与えたら、(>>=h)を返す機能を持っていて、その型は、(>>=h):: m b -> m cだと。つまり(>>=)は、関数 h を使って、m bをm cに変える関数を作り出す(高階)関数と考えるといいと思います。
   h :: b -> m c のような関数hを考えると
  (>>=h):: m b -> m c

もう1つ、(>>=)の難解な点があると思います。それは、型クラスに理由があります。Maybeモナドは、自分で(>>=)や、returnを定義しなくてもモナドとしての役割を果たします。最初のころ型クラスは知っていたもののきちんと理解していなくて、自動的に(>>=)とreturnは、使えるものと誤解していました。自分で型クラスを使ってみるとすぐわかることですが、型クラスで定義されているメソッドは、インスタンス化するときに自分で実装しないといけないというのに最初気づいていなかったことを思い出しました。(最初は使う必要を感じないので )Maybeとかは、あらかじめ用意されているだけです。

 「モナド則が謎の壁

モナド則も最初は意味が分かりませんでしたうまく説明できないのですが、モナド則は、モナドが関数っぽく振る舞うための条件じゃないかと思っています。


 M1: return a >>= f == f a
 M2: m >>= return == m
 M3: (m>>=f) >>= g  == m >>= (\x -> f x >>= g)

M1について、考えましょう。右辺は、比較的簡単なので左辺について考えます。
Haskellは、よい型システムを持っているので、型を分析しましょう。

(>>=):: m b -> (b -> m c) -> m c なので、(>>=)の第1引数は、 m bと考えることができます。
また、f は、(>>=)の第2引数になっているので、 f:: b -> m cと考えましょう。(M1にaが使われているので、型変数をb,cに選びました)。横線の式の型と(>>=)の型を比較しています。
上の方でみたように、(>>= f):: m b -> m cになるので、M1の主張は、(>>=f)にreturn aを渡すと、それは、f aになると言っています。

次に、M2について考えましょう。左辺の型を分析してみます。


一般には、(>>=)の型は、(>>=):: m b -> (b -> m c) -> m cです。
その一方、左辺のreturnの型は、(returen::b -> m b)なので、M2のmの型は (m b)でないとつじつまが合いません。
そのため、右半分(>>=より後)の型は(>>=return)::m b -> m bになります。
上の方で何度かみたように(>>=return)をモナドの変換関数とみなすと、M2の主張は、(>>=return)にmを渡すと、そのままmが返ってくるということができます。

最後に、M3について考えましょう。
M3は、右辺、左辺ともに少し複雑なので両方の型を分析します。
左辺のつじつまが合うためには、m:: m b、f::b -> mc、g::c -> m dという型になっていないといけないことは、これまでと同じように考えると分かります。

右辺についても考えます。
右辺については、ラムダ式が入っている分かりにくさが増していますが、順番に型を推論していくと、上の図のように型を割り当てることができます。(f:: b -> mc)
左辺はmから始めて、(>>=f)::m b -> m cと(>>=g)::mc -> m dを適用した結果を表しており、右辺は、括弧の中でfとgを先に合成した関数(b -> m d)を使って、(>>=(\x ->  f x >> =g)でmを変換したものが等しいと言っています。(関数の合成っぽいでしょ)

正直うまく説明できないのですが、いずれも、モナドを使って関数と関数の合成のように扱うためには成り立っていてほしい性質です。

まとめ

分からなかった頃のことを思い出しながら書いてみましたが、 モナドが分からないのは、型クラスと高階関数に慣れていないからであると思っています。両方とも、C++やJavaにて似たような概念はあるので分かった気になっていましたが、雰囲気が違うため、分かった気になっていたのです。モナドを理解するには、これら2つの概念に正しくなじむ必要があるところが、モナドを使うのには重要です。
Haskellでプログラムを書き始めたとき、型クラスや高階関数のありがたみがあまり分かりませんでした。ところが、慣れてきてくると、型クラスや高階関数を使うことで、自分で書いていたプログラムの共通点をいろいろな方法で共通化できることが分かってきました。両方に慣れてきたころ、モナドを違和感なく使えるようになったと思います。習うより慣れろかも。

まずは、型クラスと高階関数を使ってプログラムを書いてみてください。そうすれば、モナドは自然と使えるようになるよ、というのが今日の私の主張です。
こんなステップを踏むのがよいと思います。

Monadは型クラスなので、型クラスを理解しないといけないのですが、型クラスを理解するためには代数的データ型を知っておく必要があります。(左側の経路)

また、Monadの(>>=)は、高階関数なので、高階関数(map, foldl, foldr)に慣れることも重要です(右の経路)。これに加えて、高階関数に引数を与えたものを関数としてみなす(セクション)としてみなすことができると、割と(>>=)のイメージがつくと思います。