PR

計算を組み立てていくモナド

 ここまでFunctorクラスのインスタンスとして定義するデータ構造の例として,当然のようにIOを扱ってきました。読者の中には「あれっ?」っと思った方がいるかもしれません。IOのような計算を表す型を,リストやQueueなどのデータ構造(=コンテナ)と同じものとみなす考え方は,慣れない人にとっては違和感のあるものだと思います。しかし,Haskellのように関数それ自体を計算の対象=「第一級の対象(ファーストクラス・オブジェクト,first-class object)」とする関数型言語では,そのような違いは些細なものでしかありません。

 計算そのものをコンテナとみなす考え方は,モナドを理解するのに有用なものです。Monadクラスを見てみましょう。

Prelude Data.Queue Data.Tree> :i Monad
class Monad (m::* -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  fail :: String -> m a
        -- Imported from GHC.Base
instance Monad []       -- Imported from GHC.Base
instance Monad Maybe    -- Imported from Data.Maybe
instance Monad IO       -- Imported from GHC.IOBase
Prelude Data.Queue Data.Tree> 

 Functorクラスからの類推から,Monadが型構成子クラスであり,IOのような計算もコンテナとみなせることがすぐにわかると思います。

 ここで,モナドをなぜ導入したかったのかを思い出してみましょう。例えばIOモナドであれば,「順番を保証してIO処理を行いたい」ということでした。一般にモナドを使うことで,順番だけでなく様々な制御構造を導入できます。制御構造を実現するには,計算と計算を合成するための仕組みが必要になります。

 Monadクラスが用意している>>=は「bind」と読み,計算の合成をするための関数です。モナドでコンテナ(計算)を合成する場合,コンテナ内部の要素は,コンテナから取り出す直前までの計算の中間結果のうち「取り出し可能な値」になります。取り出し可能な値とわざわざ断ったことからわかるように,コンテナには「取り出し可能な値」のほかに「取り出し不可能な値」が入っていることがあります。

 Monadクラスに用意されている各関数はコンテナに対する操作なので,>>=は「コンテナにある関数を適用するための関数」,つまりfmapと同じものだと考えることができます。ではfmapと型が違うのはなぜでしょうか?

 前回紹介したprintやファイルを読み込む関数readFileのように(a -> IO b)の型を持つ関数を基に考えてみましょう。fmapで(a -> IO b)という型を持つ関数をIOというコンテナに適用すると,結果の型はIO (IO b)になってしまいます。ほしい結果はIO bなので,IO (IO b)からIO bに変換する仕組みが必要になります。こうした役割を持つ関数joinは>>=を使うことが許されるなら,join x = x >>= idというごく簡単な形で定義できます。そうでなければ,専用のjoinを定義するか,コンテナ内部の要素を取り出す(IO b -> b)のような型を持つ関数が必要となります。

 ところが,コンテナ内部の要素を取り出す処理が安全であることを,Haskellの型システムで保証することはできません。IOは,取り出し不可能な値として,「その型を使うところで行われるべき処理」を持っています。一方,(IO b -> b)という型は,取り出し可能な値を取り出すというだけの意味しか持っていません。このため,(IO b -> b)という型を持つ関数には,IO bで行われるべき処理が適切な形で組み込まれることを保証する手段は一切ありません。このように,(IO b -> b)のような関数はそれまでのコンテクストを捨ててしまうため,IOのような内部的な状態(コンテクスト)を持つコンテナと組み合わせて使用するには不都合があります。

 そこで>>=は,コンテナから取り出した値に関数を適用して再びコンテナに戻す(fmapとして使う)のにも,複数のコンテナを合成する(fmap+joinとして使う)のにも使えるように,現在の型として定義されているのです。

 IO (IO b)の場合とは逆に,「まだコンテナの外にある型aの値」を「関数を適用する対象」にするには,returnを使ってaからIO aに変換してやる必要があります。returnは,その名が示す通り,結果の値をコンテナに包んで返します。つまり,値からコンテナを作り出す関数と考えることができます。

 >>は m >> k = m >>= \_ -> kであり,要するに「m bを作るためにm aの値aを使わない」場合のコードを書きやすくするための糖衣構文(構文糖,シンタックス・シュガー,syntax sugar)です。失敗を表すfailはモナドにとって必要不可欠なものではないので,今は気にしなくても構いません。

 モナドはプログラマが望む形にコンテナを組み立てるという役割を担い,別のものがそのコンテナを利用することになります。例えば,アクションを実際の実行に変えるものは何でしょうか? それはHaskellプログラムを実行する対話環境や実行ファイルそのものです。こう書くと,Haskellが特殊なことをしているように思えるかもしれません。しかし,ほかの言語でも,実行時に必要な要素がソースコードにすべて含まれているわけではありません。

モナドの三つの法則

 Functorクラスでは二つの法則を守る必要がありました。同様に,Monadクラスのインスタンスを定義するうえでも守らなければならない制約があります。Monadクラスのインスタンスに対し,少なくとも以下の三つの法則を満たすようにreturnと>>=を定義しなければなりません。

  1. return a >>= k == k a
  2. m >>= return == m
  3. m >>= (\x -> k x >>= h) == (m >>= k) >>= h

 これらはそれぞれ,

  1. returnを作って作成したコンテナの中身を取り出し,kというコンテナを作る関数に適用することは,aという値に直接kというコンテナを作る関数を適用するのと同じであること
  2. コンテナ(上の式ではm)から取り出した値をコンテナに戻しても,同じであること
  3. モナドを使った演算の結果は,式を左右どちら側から評価しても同じになること
を要求するものです。

 これら三つの法則は,型を除けばモナドに固有のものではないために,これらの法則はよく数学的に説明されることがあります。また,よく「モナドは数学の圏論(category theory,カテゴリー論)に基づいています」といわれるため,数学を学ばなければならないと身構えてしまう方も少なくないと思います。ですが,数学の知識を持つ人にとっては,下手な類推(アナロジー)よりも数学そのもので説明したほうが明快だというだけのことに過ぎません。「Haskellを使いこなすには数学を学ばなければならない」ということを意味するわけではありません。

 要は,自分でモナドを定義したときに上の三つの法則を満たしていないなら,そのモナドはユーザーが意図しない動作を起こす可能性があるということです。現実には,モナドはたいていライブラリの中で定義するので,これらの法則を気にしなければならないのはライブラリの作者だけです。ライブラリをただ使うだけのユーザーはまず気にする必要はありません。

 この記事では特に数学を使って説明はしません。数学を使ったモナドの説明に興味のある方は,情報処理学会会誌の「情報処理」で連載されていた「Haskellプログラミング」のVol.47 No.3(2006年3月)の「自分自身を出力するプログラム」を見てください。

 さて,これでFunctorクラスとMonadクラスについては理解できたと思います。では両者の関係は実際の仕様ではどうなっているのでしょうか。

 結論を先に言ってしまうと,現在の標準Haskellでは,FunctorクラスとMonadクラスは完全に独立した形となっています。なので,モナドを定義する際にFunctorクラスを考慮する必要はありません。

 ただ,FunctorクラスとMonadクラスの共通点に着目して,FunctorクラスをMonadクラスのスーパークラス(superclass)にしたほうが良いのではないか,と考える人もいます。実際,Haskellの前身に当たる言語の一つであり,Hugsの前身でもあるGofer(参考リンク)では,MonadクラスはFunctorクラスを継承する形になっていました(また,Goferでは>>=の代わりにbind,returnの代わりにunitという関数名が使われていました。bindは束縛,unitは単位元です)。

 MonadクラスとFunctorクラス両方のインスタンスであるためには,以下の法則が成り立つ必要があります。

  1. fmap f xs == xs >>= return . f

 これは前に説明したように,>>=がfmapとして使えるものであるということを意味するものです。つまり,コンテナから取り出した値に関数を適用して再びコンテナに戻すという操作と,コンテナの中身に関数を適用するという操作が同じであることを示しています。ただし,計算の順番が逆になっていることに注意してください。

 この法則はある種のジレンマをもたらします。この法則をそのまま利用することで,Monadクラスの関数を使ってFunctorクラスのfmapを定義できます。実際GHCやHugsのIO型は,そうした形でMonadクラスとFunctorクラス両方のインスタンスとなるように定義されています。このことは,かつてのGoferのようにMonadクラスをFunctorクラスのサブクラスとして定義するべき,という人々の論拠の一つとなっています。しかし,MonadクラスをFunctorクラスのサブクラスにしてしまうと,モナドを定義するユーザーは強制的にモナド則(monad law)だけではなく,Functorが満たすべき二つの法則と両者間で満たすべき法則の計三つの法則についても関心を払わなければならなくなってしまいます。こうした制約の増加は,新たにモナドを提供する人への障壁となります。Functorとしても定義できるという選択肢は魅力的であっても,それをすべてのMonadクラスのインスタンスを定義する人に強制してしまうことは望ましくありません。

 これが,GoferではFunctorクラスのサブクラスとしてMonadクラスを定義していたにもかかわらず,現在の標準Haskellではそれぞれ独立した形で定義されている理由です。Haskell'の策定のための議論をおこなっているメーリングリストでは,サブクラスからスーパークラスの関数を定義したり,この定義を自動的に「導出する(derive)」仕組みを加えることでこうした問題を根本的に解決しよう,といった議論も行われています(参考リンク)。

今回のまとめ

 今回は

  1. モナドは「コンテナ(計算)」と「コンテナに対する操作」をセットにしたものと考えられること
  2. コンテナの値として取り出せるのは,あくまで取り出し可能な計算の中間結果であり,データそのものではないこと
  3. モナドの実行には
    1. モナドを使って計算を行う
    2. その結果できたコンテナを実際に使用する
    の二つの段階があること
を説明しました。

 Haskellではこのようなモナドの性質を利用することで,副作用(side effect)を安全に模倣(エミュレート,emulate)できます。また,状態やIOなどの副作用を対象にする計算をモナドにすることで,Haskellプログラムの中を「副作用を対象にする計算」と「副作用を対象にしない計算」に明確に分離することに成功しています。

 といっても,こうした抽象的な説明では実感がわかないかもしれません。次回以降は具体的なモナドについて紹介していきたいと思います。

do記法

 前回使用したdo記法について説明しておきましょう。do記法はモナドを使ったプログラムを書きやすくするための糖衣構文です。まずは,前回定義したprintThenAddを見てください。

printThenAdd v = do {x <- v; print x; return (x+1)}

 もしdo記法を使わなければ,この関数定義は以下のようになります。

printThenAdd v = v >>= (\x -> print x >> return (x+1))

 違いがわかったでしょうか? 下の定義では,>>=から与えられるv内の値をラムダ抽象越しに渡しています。do記法を使った定義では,代わりに<-を使って取り出したv内の値をxという変数に束縛しています。

 このくらいの長さのコードであれば,両者の違いはあまりありません。しかし,変数xを明示的にラムダ抽象を使って取り出す方法では,カッコが何重にもネストしていくにつれ,コードを読み解くのが難しくなっていきます。do記法はそうしたコードの煩雑さを解消するのに役立ちます。


著者紹介 shelarcy

 今回モナドを取り扱うことに決めた理由の一つは,先月(2006年9月)の上旬,つまり丁度前回の記事が掲載された頃に,日本の関数型言語界隈の一部のブログが「IOモナドがどうやって実行されるのか」という話題で盛り上がっていたためです(参考リンク1参考リンク2)。それぞれの人が自分の持っている知識を使って独自の切り口でIOモナドを語ろうとするこれらの試みは,「私ならこう語る」という気持ちを呼び起こさせるものでした。

 幸い前回多相性について扱ったので,今回モナドについて語っても大丈夫だろうと判断しました。そこで「ほとんどの人がなんとなく理解しているものの,あまり語られることのない側面」についても紹介することを目標にモナドの話を始めてみましたが,ここまでのところはいかがだったでしょうか? 次回以降も,そうした目に見えない部分を明らかにするべく掘り下げていこうと思います。