Prev: クラスで使う TOC: 目次 Next: 練習問題

モナド則


このチュートリアルではいままで、技術的な議論を避けてきました。しかし、 モナドについて考えるべき技術的な要点が2、3あります。モナド演算は、 「モナド公理」として知られている、いくつかの法則群に従わなければ なりません。これらの法則は Haskell のコンパイラが強制するものでは ありません。したがって、すべての Monad のインスタンスと 宣言したものが、これらの法則に従うことを保証するのはプログラマ自身です。 Haskell の Monad クラスは、まだ見ていませんが、最小限の定義 以上にいくつかの関数を含んでいます。結局、多くのモナドは標準のモナド則 以外の規則にも従っています。Haskell のクラスにはこうした拡張されたモナド をサポートするためのものがあります。

三つの基本則

モナドの概念は圏論とよばれる数学の分野から来たものです。モナドを つくったり、つかったりするのに、圏論を知る必要はありませんが、数学的な 形式にはすこしばかり従う必要があります。モナドを作るためには、Haskell の Monad クラスのインスタンスであるということを正しく 型シグネチャをつかって宣言しただけでは十分ではありません。正しいモナドで あるためには return および >>= 関数がともに 以下の3つの法則をみたさなければなりません。

  1. (return x) >>= f == f x
  2. m >>= return == m
  3. (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 モナドを使わないことが 保証されているということです。これ以外の、ListMaybe ではモナドから値を取り出すことが可能です。したがって、 これらモナドを内部的に用いても、モナド以外の値を返す関数を書くことが できます。

一方向モナドの素晴しい機能は、プログラムのモナドではない部分の関数型 の性質を破壊することなく、そのモナド演算子中で副作用をサポートすることが できるというものです。

ユーザ入力から一文字読むという単純な問題を考えてみましょう。 単に関数 readChar :: Char とかを使えばよいという わけにはいきません。ユーザの入力に依存して、呼ばれるたびに別の文字を 返す必要があるからです。すべての関数は同じ引数で呼ばれれば必ず同じ値を 返すというのは Haskell が純粋な関数型言語であるための本質的な性質です。 しかし、IO モナド中で、I/O関数 getChar :: IO Char を使うことには 全く問題ありません。一方向モナド内の一つのシーケンス中でしか 使われないからです。これを使うどのような関数のシグネチャーからも、 型構築子 IO を除去する方法はありません。つまり、 型構築子 IO はI/Oを行うすべての関数を識別する一種の タグとして働きます。さらに、このような関数は IO モナド 中でしか利用することはできません。それゆえ、一方向モナドは純粋な 関数型言語のルールを緩和できる計算領域を効果的に隔離して作りだすこと ができます。関数型の計算をこの領域に移すことができますが、危険な 副作用や非参照透明関数は避けることができます。

モナドを定義する際のもうひとつのよく使われるパターンは、モナドの 値を関数で表現することです。そうしておいて、モナド計算の値が必要に なったときに、結果のモナドを「走らせて(run)」、答えを提供します。

Zero と Plus

上述の3つのモナド則以外に、いくつかのモナドが従う付加的な規則があります。 このようなモナドには、以下の4つの規則に従う、特別な値 mzero 特別な演算 mplus をもつものがあります。

  1. mzero >>= f == mzero
  2. m >>= (\x -> mzero) == mzero
  3. mzero `mplus` m == m
  4. 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 演算子は別々の計算を合成してひとつのモナド計算にする のに使われます。羊クローンの例では Maybemplus をつかって、 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: 練習問題