人気ブログランキング | 話題のタグを見る

All About Monads 読解 5

The monad laws

Monad クラスの多相関数 return と >>= は自由に実装できる訳ではなく、モナド則というルールを満たす必要がある。モナド則を実装するかどうかは Haskell コンパイラは関与しないので、自前の実装が必要だ。また、多くのMonad クラスは、基本的なモナド則いがいに、付加的な機能を持っている。

The three fundamental laws

ユーザ定義のデータ型をモナドクラスのインスタンスにして、return と >>= を実装してもそれだけではそのデータ型がモナドになる訳ではない。そのデータ型がモナドになるためには、次の3つのモナド則を return と >>= が満たさなければならない。

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

このモナド則は return が >>= 演算子について左単位元である事、同時に右単位元でもあること、>>= 演算子が結合法則を満たしている事を要求している。

どんなタイプコンストラクタでも、return 多相関数と >>= 演算子をもち、それらが、モナド則をみたしていれば、それはモナドだ。

単位元だ、結合法則だといってもそれは圏論の言葉で考えないと意味をなさないし、モナドのプログラムを組むのに圏論の知識が必要という訳でもない。自前のモナドを作るためにはモナド則が必要なのだくらいの理解でも十分プログラムは作る事ができる。

Failure IS an option

Monad クラスの fail と >> という2つの多相関数は、モナドに必須な関数という訳ではなく、付加的なものだ。

デフォールトの fail の実装は次のようになっている。

fail s = error s

特に必要がない限り、この fail の定義をユーザが変更する必要はない。たとえば、Maybe モナドでは fail の定義は次のようになっている。

fail _ = Nothing

ghci で試してみた。

Prelude> fail "dummy" :: Maybe Int
Nothing

ちなみに、IO モナド、リストモナドでは次のようになる。

Prelude> fail "dummy" :: IO Int
*** Exception: user error (dummy)
Prelude> fail "dummy" :: [Int]
[]

fail は数学理論によるモナドには必須の要件ではない。しかし、モナドプログラミングを do 記法を使って記述するときに、ブロック内でパターンマッチの失敗が起きると fail が呼び出される。

Prelude> :set +m
Prelude> let
Prelude| fn :: Int -> Maybe [Int]
Prelude| fn idx = do
Prelude|   let l = [Just [1,2,3], Nothing, Just [], Just [7..20]]
Prelude|   (x:xs) <- l !! idx
Prelude|   return xs
Prelude|
Prelude> fn 0
Just [2,3]
Prelude> fn 1
Nothing
Prelude> fn 2
Nothing
Prelude> fn 3
Just [8,9,10,11,12,13,14,15,16,17,18,19,20]

>> 演算子は、モナドのプログラムをするときに、関数が前段の戻値を必要としないときに使われる。>> は >>= を使って次のように定義される。

(>>) :: m a -> m b -> m b
m >> k = m >>= (\_ -> k)

>> の例を ghci で実行したものを次に示す。

Prelude> do putStr "hello, " >> putStrLn "world"
Prelude|
hello, world

No way out

標準的な Monad クラスに定義されている多相関数では、モナド内の値が取り出せないのに気づいた人もいるだろう。しかし、それは仕様だ。もっとも、モナドの値を取り出そうと思えば、たとえば Maybe モナドの場合は、プログラマは Just x のパターンマッチや、fromJust 関数を使ってモナド値から値を取り出すことができる。

しかし、モナドの値を取り出す必要がなければ、Monad クラスは一方向モナドをつくりだしてくれる。一方向モナドは外部から値を取り入れたり ( return x ) モナドの関数間で値を渡したり ( >>= や >> の bind 演算子) できるが、モナドの外から値を取り出すことはできない。

IO モナドが一方向モナドの馴染み深い例だ。IOモナドの中では、IO a 型の値を戻すような関数しか使うことができないため、プログラマは一旦 IO モナドに入ると、そこから脱出することはできない。Maybe モナドや List モナドの場合はそれほど厳しい規制はなく、値を外に取り出せる。

一方向モナドの最も素晴らしい点は、副作用のある関数を IO モナドの中に閉じ込めて、モナドの外のプログラムから隔離してしまうことができるところだ。

たとえば、IO モナドの readChar :: Char 関数は、標準入力から一文字を読み取って IO Char として返す関数だが、readChar は呼び出されるたびに戻値が異なってしまう。これは、同じ入力に対しては同じ値を出力するという関数の大原則を壊してしまっている。しかし、readChar の戻値は IO タグが付いているので、IO モナド以外の環境では使うことができない。このため、副作用のある関数は IO モナドの中でしか使えないし、モナドの外部からはその値を利用することができない。このように、一方向モナドは副作用のある関数を閉じ込めて隔離してしまうことができるので、モナドの外のプログラムは副作用のことは考慮せずに記述することができるのだ。

また、モナドのプログラミングのもう一つの特徴は、モナドのコンテナの値を関数にできることだ。このばあい、モナドの値が必要になったら、run コマンドの引数としてモナドを与えることで、モナドのコンテナの関数を実行してモナド内の値を取り出すことができる。

Zero and Plus

基本的なモナド3則の他に、付加的な計算規則を持つモナドもある。このようなモナドは mzero という値と mplus という演算子を持つ。これらの値と演算子は次の4つの法則に従わなければならない。

1. mzero >>= f == mzero
2. m >>= (\x -> mzero) == mzero
3. mzero `mplus` m == m
4. m `mplus` mzero == m

この mzero と mplus に関する法則は mzero を 0、mplus を +、>>= を × の数の計算に置き換えると覚えやすい。

この付加的な mzero と mplus は MonadPlus クラスの多相関数だ。Maybe 型は MonadPlus のインスタンスたが、その実装は次のようになっている。

instance MonadPlus Maybe where
    mzero = Nothing
    Nothing `mplus` x = x
    x `mplus` _ = x

ghci で試してみた。

Prelude> :m Control.Monad
Prelude Control.Monad> mzero :: Maybe Int
Nothing
Prelude Control.Monad> Nothing `mplus` Just 1
Just 1
Prelude Control.Monad> Just 1 `mplus` Just 2
Just 1
Prelude Control.Monad> Just 2 `mplus` Nothing
Just 2

Maybe モナドの mzero は Nothing で mplus は引数のうち Nothig でない方の最初の値が返されるのがわかる。

リスト・モナドも MonadPlus のインスタンスだが、mzero は [] で mplus は ++ 演算子だ。

mplus 演算子は別々の計算値を一つのモナド値に統合するのに使われる。たとえば以前の記事で述べた羊の血統を計算するプログラムでは、Maybe モナドを使って羊の両親を検索するプログラムを次のように定義できる。

parent s = (mother s) `mplus` (father s)

この場合は、羊に母親か父親がいる場合はそのうちの一匹が値として返される。

Summary

Monad クラスのインスタンスはモナド則を満たしている必要がある。その規則はモナドの代数学的な規則からきている。モナド則の最初の2つは、return 関数が左単位元と右単位元であることを意味しており、3番目の法則は >>= 演算が結合法則に従っていることを示している。この3原則に従わない実装では、do 記法がきちんと働かない。

return と >>= 関数に加えて、Monad クラスには fail 関数が登録されている。fail クラスはモナドの働きには直接には関与していないが、do 記法の中でエラーが発生するような場合に有用だ。

MonadPlus クラスの関数 mzero と mplus は、モナドに付加的な計算を加えてモナドの計算を拡張する。

前へ 目次 次へ
by tnomura9 | 2013-05-17 21:42 | Haskell | Comments(0)
<< All About Mona... 橋下発言について >>