Prev: クラスで使う | TOC: 目次 | Next: 練習問題 |
このチュートリアルではいままで、技術的な議論を避けてきました。しかし、
モナドについて考えるべき技術的な要点が2、3あります。モナド演算は、
「モナド公理」として知られている、いくつかの法則群に従わなければ
なりません。これらの法則は Haskell のコンパイラが強制するものでは
ありません。したがって、すべての Monad
のインスタンスと
宣言したものが、これらの法則に従うことを保証するのはプログラマ自身です。
Haskell の Monad
クラスは、まだ見ていませんが、最小限の定義
以上にいくつかの関数を含んでいます。結局、多くのモナドは標準のモナド則
以外の規則にも従っています。Haskell のクラスにはこうした拡張されたモナド
をサポートするためのものがあります。
モナドの概念は圏論とよばれる数学の分野から来たものです。モナドを
つくったり、つかったりするのに、圏論を知る必要はありませんが、数学的な
形式にはすこしばかり従う必要があります。モナドを作るためには、Haskell の
Monad
クラスのインスタンスであるということを正しく
型シグネチャをつかって宣言しただけでは十分ではありません。正しいモナドで
あるためには return
および >>=
関数がともに
以下の3つの法則をみたさなければなりません。
(return x) >>= f == f x
m >>= return == m
(m >>= f) >>= g == m >>= (\x -> f x >>= g)
最初の規則は return
が >>=
に関して左単位元に
なっていることを要請しています。二番目の規則は return
が
>>=
に関して右単位元になっていることを要請しています。
そして、三番目の規則は >>=
に関する一種の結合法則です。
三番目の規則に従えば、モナドをつかった do 記法のセマンティクスは一貫性を
もちます。
この3つのモナド則を満すリターンおよびバインド演算子をもつ型構築子は
すべてモナドです。Haskell においては、これらの規則が Monad
クラスのすべてのインスタンスで保持されているかどうかを、コンパイラが
チェックすることはありません。プログラマがつくった Monad
の
インスタンスがどれもモナド則を満すようにするのは、プログラマ自身の
仕事です。
前述 の Monad
クラスの定義は
最小限の定義をしめしたものにすぎません。実際の Monad
クラスの定義にはさらに2つの関数、fail
と >>
が
あります。
fail
関数のデフォルト実装は、
fail s = error s |
です。
失敗に対して別の振舞いを提供したいわけではないのなら、自分のモナド用に
この定義を変更する必要はありません。あるいは、失敗を自分のモナドの
計算戦略に組込む必要はないのです。たとえば、Maybe
モナドでは fail
は以下のように定義されています。
fail _ = Nothing |
こうすると fail
は、Maybe
モナド中で別の
関数で束縛されたときに、意味のある Maybe
モナドの
インスタンスを返すようになります。
fail
関数は、モナドの数学的定義においても要請されている
部分ではありません。しかし、標準の Monad
クラスの定義に
含まれているのは、Haskell の do 記法での役割りがあるからです。
fail
関数は、do ブロック中でパターンマッチに失敗したときに
必ず呼ばれます。
fn :: Int -> Maybe [Int] fn idx = do let l = [Just [1,2,3], Nothing, Just [], Just [7..20]] (x:xs) <- l!!idx -- パターンマッチに失敗すると "fail" を呼ぶ return xs |
それゆえ、上のコードでは、fn 0
は値 Just [2,3]
をもちますが、fn 1
および fn 2
の両方は
Nothing
の値をもちます。
>>
関数はモナド計算はバインドするけれど、ならびの中で、
前の計算からの値を必要としない場合に便利な演算子です。この関数は
>>=
を使って定義されています。
(>>) :: m a -> m b -> m b m >> k = m >>= (\_ -> k) |
もうお気づきですか。標準の Monad
クラスでは
モナドから値を得る手段は定義されていません。これは決して、
事故ではありません。特定のモナドの作者がそのモナドに対して、
値を得る手段を妨げるものはありません。たとえば、Maybe
モナドから、Just x
上のパターンマッチング、あるいは
fromJust
関数を用いて、値を取り出すことができます。
Haskell の Monad
クラスでは、このような関数を要求しない
ことで、一方向モナドの生成を可能にしています。一方向モナド
は値を、return
関数(場合によっては fail
関数)を通じて、モナドに入れることが可能で、モナド中の計算は、
バインド関数 >>=
および >>
を用いて実行する
ことができます。しかし、モナドから値を逆にとり出すことはできません。
IO
モナドは Haskell におけるよく知られた一方向モナドの
例です。IO
モナドから脱出することはできませんから、
IO
モナド中で計算を行うにもかかわらず、結果の値として
型構築子 IO
を含まないような関数の定義を書くことは不可能です。
どういう意味かというと、型構築子 IO
を含まないような結果を
返すあらゆる関数は IO
モナドを使わないことが
保証されているということです。これ以外の、List
や
Maybe
ではモナドから値を取り出すことが可能です。したがって、
これらモナドを内部的に用いても、モナド以外の値を返す関数を書くことが
できます。
一方向モナドの素晴しい機能は、プログラムのモナドではない部分の関数型 の性質を破壊することなく、そのモナド演算子中で副作用をサポートすることが できるというものです。
ユーザ入力から一文字読むという単純な問題を考えてみましょう。
単に関数 readChar :: Char
とかを使えばよいという
わけにはいきません。ユーザの入力に依存して、呼ばれるたびに別の文字を
返す必要があるからです。すべての関数は同じ引数で呼ばれれば必ず同じ値を
返すというのは Haskell が純粋な関数型言語であるための本質的な性質です。
しかし、IO
モナド中で、I/O関数
getChar :: IO Char
を使うことには
全く問題ありません。一方向モナド内の一つのシーケンス中でしか
使われないからです。これを使うどのような関数のシグネチャーからも、
型構築子 IO
を除去する方法はありません。つまり、
型構築子 IO
はI/Oを行うすべての関数を識別する一種の
タグとして働きます。さらに、このような関数は IO
モナド
中でしか利用することはできません。それゆえ、一方向モナドは純粋な
関数型言語のルールを緩和できる計算領域を効果的に隔離して作りだすこと
ができます。関数型の計算をこの領域に移すことができますが、危険な
副作用や非参照透明関数は避けることができます。
モナドを定義する際のもうひとつのよく使われるパターンは、モナドの 値を関数で表現することです。そうしておいて、モナド計算の値が必要に なったときに、結果のモナドを「走らせて(run)」、答えを提供します。
上述の3つのモナド則以外に、いくつかのモナドが従う付加的な規則があります。
このようなモナドには、以下の4つの規則に従う、特別な値 mzero
特別な演算 mplus
をもつものがあります。
mzero >>= f == mzero
m >>= (\x -> mzero) == mzero
mzero `mplus` m == m
m `mplus` mzero == m
mzero
を 0 に、mplus
を + に、そして、
>>=
を × という算術演算にそれぞれ対応させれば、
mzero
および mplus
の法則を覚えるのは簡単です。
ゼロとプラスをもつモナドは Haskell では MonadPlus
クラスの
インスタンスとして宣言できます。
class (Monad m) => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a |
例として Maybe
モナドをもうすこし使ってみましょう。
Maybe
モナドが MonadPlus
のインスタンスで
あることが分ります。
instance MonadPlus Maybe where mzero = Nothing Nothing `mplus` x = x x `mplus` _ = x |
これは、Nothing
をゼロ値として認識し、ふたつの
Maybe
値の加法は最初の Nothing
ではない
値であるということです。両方の値が Nothing
なら、
mplus
の結果の値も Nothing
です。
リストモナドにもゼロとプラスがあります。mzero
は
空リストで、mplus
は ++
演算子です。
mplus
演算子は別々の計算を合成してひとつのモナド計算にする
のに使われます。羊クローンの例では Maybe
の
mplus
をつかって、
parent s = (mother s) `mplus` (father s)
という関数を定義できます。この関数は、もし親が存在すればその親を返し、
どちらの親もいなければ Nothing
を返します。両親がいる場合に
はこの関数は、Maybe
モナド中の mplus
の定義に
よって、どちらかの親を返します。
Monad
クラスのインスタンスは、所謂、モナド則を
満さなければなりません。モナド則はモナドの代数的性質を記述するものです。
このような規則には3つあって、return
関数は、左単位元であり
かつ右単位元であり、束縛オペレータは結合性をもつと主張しています。
これらの法則を満す「失敗」は、正しく動作せず do 記法を使った場合に深刻な
問題になるようなモナドの結果となります。
return
および >>=
関数に加えて、
Monad
クラスにはもうひとつの関数、fail
が
定義されている。fail
関数は、モナドにそれを含める技術的
要請があるわけではありませんが、多くの場合、実際に便利であるのと、
Haskell の do 記法で用いられるので、Monad
クラスには
含まれています。
モナドには3つの基本則以外の規則にも従うものがあります。そのような
モナドのクラスで重要なものにゼロ要素の記法と加法演算子をもつものが
あります。Haskell では、MonadPlus
クラスが、このような
mzero
値と mplus
演算子をもつモナドとして
提供されています。
Prev: クラスで使う | TOC: 目次 | Next: 練習問題 |