Haskell 入門


リストモナド

リストもモナドらしい。そう言われてもなかなかイメージできない。しかし、IOモナドに取り組んだおかげで、モナドの本質を簡単に言い表せるようになった。

モナドの本質

それでは、モナドの本質とはなんだろうか。モナドの本質とは、「モナドのデータは関数で写像してもモナドのデータだ」ということだ。そうして、モナドのデータを流し込むことができる唯一の関数は、「引数がひとつで戻り値がモナド型の関数」というモナド型の関数なのだ。モナドのデータを、>>= 演算子で、モナド型の関数に流し込んでやると、戻り値にモナド型のデータが帰ってくる。モナドの本質とはたったそれだけのことだ。

例えばIOモナドについて言えば、IO a 型のデータを「引数一個、戻り値 IO b 型」の IO モナド型関数 f に >>= 演算子で流し込んでやると IO b 型のデータが戻ってくる。次の例は IO "hello" を >>= で putStrLn 関数に流し込んでやると IO () が戻り値として返ってくる。

Prelude> return "hello" >>= putStrLn
hello

リストモナド

リストモナドの場合も同じように考えればいい。[1,2,3] というリストモナド型のデータを、リストモナド型の関数 return に >>= で注ぎ込むと、リスト [1,2,3] が返ってくる。

Prelude> [1,2,3] >>= return
[1,2,3]

>>= も return も IO モナドでお馴染みの関数だが、リストモナドでは動作が違う。いわゆる多型性というやつだ。

IOモナドでは >>= は IO a から a を取り出して右辺の引数1個の関数へ渡していたが、リストモナドでは、リストの要素をひとつずつ取り出して、右辺のやはり引数一個の関数に渡す。リストモナドでは、それぞれの要素に関数を適用した結果が気になるが、return 関数に結果を渡してやるとそれは再びリストにまとめられる。IOモナドの場合の a を IO a にくるんで返すのと同じ形式だ。

したがって、IOモナドのときに引数1個の関数を return と合成し、return . f とすることでIOモナド型の関数を作ったときと同じに、リストモナドの場合も return . f でリストモナド型の関数を作ることができる。

たとえば、return . (*2) はリストの個々の要素を2倍してリストにまとめた戻り値を返すリストモナド型の関数になる。

Prelude> [1,2,3] >>= return . (*2)
[2,4,6]

この例でも分かるように、リストモナドのデータである [1,2,3] というデータをリストモナド型の関数に >>= で注ぎ込むと、リストモナドのデータである、[2,4,6] が返ってくる。このへんの事情はIOモナドの場合とまったく同じだ。

リストモナドの連結

また、IOモナドの場合も、IOモナド型関数は >>= で数珠繋ぎにすることができたが、リストモナドでも同じようなことができる。

Prelude> [1,2,3] >>= return . (*2) >>= return . (+1)
[3,5,7]

さらに、IOモナドではIO aの値をラムダ記法を使うことによって変数に束縛することができたが、リストモナドでも同じことができる。

Prelude> [1,2,3] >>= \x -> [4,5] >>= \y -> return (x,y)
[(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]

そうであれば、do 記法は使えるだろうか。答えはイエスだ。

Prelude> do x <- [1,2,3]; y <- [4,5]; return (x + y)
[5,6,6,7,7,8]

リストの内包表記

リストの関数には内包的定義がある。二つのさいころの目の組み合わせを一気に作ってくれる便利な記法だ。

Prelude> [(x,y)| x <- [1..6], y <- [1..6]]
[(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(2,1),(2,2),(2,3),(2,4),(2,5),(2,6),
(3,1),(3,2),(3,3),3,4),(3,5),(3,6),(4,1),(4,2),(4,3),(4,4),(4,5),(4,6),
(5,1),(5,2),(5,3),(5,4),(5,5),(5,6),(6,1),(6,2),(6,3),(6,4),(6,5),(6,6)]

しかし、次のようにすると上のリストモナドの結果と同じになる。

Prelude> [ x + y | x <- [1,2,3], y <- [4,5] ]
[5,6,6,7,7,8]

リストの内包的定義は、Haskell のほかの関数とは異質な感じがしていたが、実はリストモナドを使ってちゃんと Haskell で定義できていたのだ。

モナド則

最後に、リストモナドがモナド則を満たしているかどうか実験してみよう。モナドをプログラミングするのに、モナド則はいらないが、自前のモナドを定義するときには必要になる(らしい)。

モナド則は >>= 演算子について次のような法則が成り立つことを要求する。

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

1番目の規則は return が >>= について左単位元であることを示している。つまり、return を左から関数 f に作用させてももとの f と同じということだ。

Prelude> ([1] >>= return . (*2)) == ((return . (*2)) 1)
True

2番目の規則は、return が >>= 演算子について右単位元になっていることを示している。つまり [1] を return に注ぎ込んでも、同じ [1] が帰ってくる。

Prelude> [1] >>= return
[1]

3番目の規則は >>= に結合法則が成り立つことを示している。つまり、最初の >>= を計算して次の >>= にデータを送った結果が、右の >>= を先に適用した合成関数に左端の >>= を使ってデータを送っても同じ結果になるということ。

Prelude> (([1] >>= return . (*2)) >>= return . (+1)) ==
([1] >>= \x -> (return . (*2)) x >>= return . (+1))
True

ちょっと話が込み入ってきたが、モナド則など無視しても、モナドのプログラミングには別に影響ない。