Prev: イントロダクション TOC: 目次 Next: class で使う

モナドに触れる


この章をつうじて、Maybe 型構築子を使うので、先にすすむ前に Maybe の使いかたと定義になれておかなければなりません。

型構築子

Haskell のモナドを理解するためには、型構築子の扱いになじんでいる必要があ ります。型構築子 は多相型をもちいたパラメータ化された型定義です。 Haskell では、一つ以上の具体的な型にたいして、型構築子を適用すると 新しい具体的な型を構築できます。Maybe の定義は、

  data Maybe a = Nothing | Just a
です。Maybe は型構築子で Nothing および Just はデータ構築子です。Just データ構築子を ある値に適用してデータ値を構成できます。
  country = Just "China"
同様に、Maybe 型構築子をある型に適用して型を構成できます。
  lookupAge :: DB -> String -> Maybe Int

多相型は多くの異る型の値を保持できるコンテナのようなものです。それゆえ、 Maybe IntInt の値(あるいは Nothing)を保持する Maybe コンテナと看倣せ、 また、Maybe StringString の値(あるいは Nothing)を保持する Maybe コンテナと看倣せます。 Haskell ではこのコンテナの型も多相にすることができます。それゆえ、 「m a」と書いて、ある型の値を保持するある型のコンテナを表現 することができます。

計算の抽象的な機能を表現するのに型構築子とともに型変数をよく使います。 たとえば、多相型 Maybe a は、ある値または Nothing を返し得るすべての計算の型です。同様に、コンテナが どのようなものを保持するかという詳細から離れて、コンテナの性質について 議論することができます。

モナドを使っていて、コンパイラが「kind errors」に関するエラーを出したら、 それは、型構築子を正しく使っていないという意味です。

Maybe というモナド

Haskell ではモナドは型構築子(これを m と呼ぶことにします)、 その型の値を構築する関数(a -> m a)、および、 その型の値とその型の値を生成する計算とを組合せて、その型の新しい計算を 生成する関数 (m a -> (a -> m b) -> m b) で表現されます。ここで、コンテナは同じですが、コンテナの内容の型は変りう ることに注意してください。モナドの型構築子を「m」とするのは、 モナドに関して一般的な議論をするときの習慣です。この型の値を構築する関数 は伝統的には「return」とよばれます。また 3 つめの関数は 「bind」として知られていますが、「>>=」と書きます。 これらの関数のシグネチャは

-- モナド m の型
data m a = ... 

-- return はモナドのインスタンスを作る型構築子 
return :: a -> m a

-- bind はモナドのインスタンス m a と、 
-- a から別のモナドのインスタンス m b を作る計算と
-- を組み合わせて新しいモナドのインスタンス m b を作る
(>>=) :: m a -> (a -> m b) -> m b
となります。

おおざっぱに言うと、モナドの型構築子は計算の型を定義し、 return 関数はこの計算の型のプリミティブな値を生成し、 >>= はこの型の計算を組み合わせて、この型のより複雑な計算を 作り出します。コンテナのアナロジーを用いれば、型構築子 m は 異なる値を保持できるコンテナです。m a は型 a の値を保持するコンテナです。return 関数は一つの値を、一つの コンテナにいれる関数です。>>= 関数はモナドからその値をとり、 それを新しい値(別の型の可能性もある値)を含むモナドコンテナをつくる関数に 渡します。>>= 関数は、モナドコンテナ中の値を関数の第一引数 に束縛するので、「bind」として知られています。この束縛関数にロジックを追 加することで、モナドはそのモナド中の計算を組合せる特定の戦略を実装するこ とができます。

このことは、下の例を見たあとならわかりやすくなってきますが、この点について、 ひどく訳がわからない感じがするなら、次に進む前に、ここの モナドの物理的なアナロジーを見ると いいでしょう。

ひとつの例

羊のクローン実験の記録をとるプログラムを書いているとしましょう。すべての 羊の遺伝履歴を知りたいでしょうから、motherfather という関数が必要になるでしょう。でも、クローン羊なの で常に母と父の両方がいるとは限りません。

Haskell のコードでは型構築子 Maybe をつかって、母あるいは 父をもたない可能性を表現しましょう。

type Sheep = ...

father :: Sheep -> Maybe Sheep
father = ...

mother :: Sheep -> Maybe Sheep
mother = ...

こうすると、祖父を見つける関数の定義は少し複雑です。どちらかの親をもたない 可能性を処理しなければならないからです。

maternalGrandfather :: Sheep -> Maybe Sheep
maternalGrandfather s = case (mother s) of
                          Nothing -> Nothing
                          Just m  -> father m
さらに、ほかの祖父母の組合せを処理しなければなりません。

曾祖父を見つけようとするとさらに状況は悪くなります。

mothersPaternalGrandfather :: Sheep -> Maybe Sheep
mothersPaternalGrandfather s = case (mother s) of
                                 Nothing -> Nothing
                                 Just m  -> case (father m) of
                                              Nothing -> Nothing
                                              Just gf -> father gf

醜く、不明瞭で、保守しにくいばかりでなく、これではあまりに面倒です。 計算途中での Nothing値はどこにあっても最終的な結果を Nothing にしてしまうということは明かです。このことの記述を 一箇所に集約し、コードのなかにまき散らされているすべての明示的 case による検査を取り除けば、コードはずっと良くなる ことでしょう。こうすれば、コードは書きやすく、読みやすく、変更しやすい ものになるでしょう。よいプログラミングスタイルは求める振舞いを捕捉する 組合せ子をつくることです。

example1.hs で利用可能なコード
-- comb は Maybe を返す操作の並びに対する組合せ子
comb :: Maybe a -> (a -> Maybe b) -> Maybe b
comb Nothing  _ = Nothing
comb (Just x) f = f x

-- こうすると `comb` をつかって複雑な並びを構築できる
mothersPaternalGrandfather :: Sheep -> Maybe Sheep
mothersPaternalGrandfather s = (Just s) `comb` mother `comb` father `comb` father 

この組合せ子は大成功です。コードはずっと明瞭で書きやすく、理解も変更もし やすいものになりました。comb 関数は完全に多相的であることに も注目してください。これは決して Sheep に特定化されたもので はないということです。実際、この組合せ子は、値を返すのに失敗する可能 性のある計算を合成するための一般的戦略を捕捉しています。 したがって、 同じこの組合せ子を、データベースへの問い合わせや辞書の検索などのように、 値を返せない可能性のある計算に適用することができます。

嬉しい結果は、プログラミング実践の常識が企らずもモナドの創造につながった ということです。Just関数(return のように振舞う) をともなう型構築子 Maybeと、いま定義した組合せ子 (>>= のように振舞う)とで、値を返さない可能性のある計算を 構築する単純なモナドを構成します。このモナドを本当に役に立つものにするた めにあとやるべきことは、Haskell 言語で構築されたモナドのフレームワークに すりあわせことだけです。これは次章の主題です。

リストもモナド

型構築子 Maybe が、値を返さない可能性のある計算を構築するた めのモナドであることを見ました。もうひとつ、Haskell ではおなじみのリスト を構築する型構築子 []がモナドであると知ればびっくりするかも しれませんね。リストモナドを使えば、0個、1個、あるいはもっと多くの値を返 しうる計算を構成することができます。

リストの return 関数は単純に一要素のリストを生成します (return x = [x])。リストの束縛演算は元のリス トに含まれるすべての値にたいして、関数を適用した結果を含む新しいリストを 生成します (l >>= f = concatMap f l)。

リストを返す関数のひとつの使い道は、曖昧な計算 — すなわち、 0個、1個あるいはそれ以上の出力を許すような計算を表現することです。 曖昧な部分計算から組み合わされた計算のなかでは、曖昧さも合成されます。 あるいは、偶々一つの値だけの出力も可能であり、ひとつの出力もないというこ とも可能です。この過程において、可能な計算状態の集合はリストによって表現 されます。したがって、リストモナドは、曖昧な計算のすべての許されたすべて パスにそった、同時並行計算を行う戦略を具現しています。

List モナドのこの使い方の例と、Maybe モナドの例との対比はすぐに示します。 しかし、まず、Haskellでは有用なモナドがどのように定義されているかをみて おかなければなりません。

要約

モナドが、型構築子と、return と呼ばれる関数と、 bind あるいは >>= とよばれる組合せ子関数で あることを見ました。この3つの要素が共に機能して、計算の合成戦略を カプセル化し、さらに複雑な計算を創りだします。

型構築子 Maybe を用いて、良いプログラミングの実践により、 それぞれが値を返すのに失敗するような計算の並びから複雑な計算を 構築するために利用できる単純なモナドをどのように定義するかを見ました。 Maybe モナドの結果は値を返さないかもしれない計算を合成する 戦略をカプセル化しています。モナド中のこの戦略をプログラムコードにする ことで、アドホックな方法で組合せた計算にはない、モジュラリティと 柔軟性を達成できました。

また、もうひとつのよくある Haskell の型構築子、[] もモナド であることを見ました。リストモナドは 0個、1個あるいはそれ以上の複数の 値を返し得る計算の組合せ戦略をカプセル化したものです。


Prev: イントロダクション TOC: 目次 Next: class で使う