やさしい Haskell 入門 (バージョン 98 )
back next top


9  モナドについて

はじめて Haskell にふれる多くのひとにとって モナド ( monad ) の概念は謎です。Haskell ではモナドに頻繁に出会います。 I/O システムはモナドによって構成されていますし、モナドのための特別な構文 ( do 式)も用意されています。また、モナドのためだけのモジュール も標準ライブラリに含まれています。このセクションではモナドを使ったプログ ラミングについて詳細にみていきましょう。

このセクションは他のセクションにくらべるとおそらく「やさしい」というわけ にいかないでしょう。ここでは、モナドをふくむ言語の特徴を示すだけではなく、 もっと壮大な絵を明かにしようとおもいます。なぜモナドがこれほど重要なツー ルであり、どのようにこれを使うか、ということです。だれにとっても、わかる モナドの説明は、これだ、というものはありません。詳細な説明は、 haskell.org から辿れるでしょう。モナドをつかった、実際のプログ ラミング入門としてよい文献として Wadler の Monads for Functional Programming [10] をあげ ておきましょう。

9.1  モナドのクラス

プレリュードには Haskell でつかうモナドを定義したクラスがいくつもありま す。これらのクラスは圏論( category theory )におけるモナドの構成を基礎と しています。圏論の用語はモナドのクラスや演算にその名前をのこしていますが、 モナドのクラスの使い方を直観的に理解するにはこういった抽象数学に通じてい る必要はありません。

モナドは IO のような多相型の上に構成されます。モナド自身はイン スタンス宣言によって定義されます。このインスタンス宣言には、 FunctorMonad および MonadPlus というモナド のクラスの一部あるいは全部の型にたいするものです。モナドのクラスはどれも 導出されることはありません。IO のほかに、リスト ([]) と Maybe というモナドクラスの型がプレリュードに定義されています。

数学的にはモナドは、モナドの演算に対する一連の法則により支配されています。 この法則の概念はなにもモナドに限ったことではありません。Haskell ではほか にも、すくなくとも非公式には、法則に支配されている演算があります。例をあ げると x /= ynot (x == y) とは比較されるあらゆる型の値に対して同じでなければなりません。しかし、こ れを保証するものはなにもありません。==/= とは Eq クラスのなかでは別のメソッドです。しかも、===/ とが関連をもつものであることを保証する手段はありません。 おなじように、ここでしめすモナドの法則は、Haskell では保証できません。し かし、モナドのクラスのインスタンスであれば、どれも満すべき法則です。モナ ドの法則はモナドの構造の底流にあるものへの洞察をあたえるものです。これら の法則を検証しながら、モナドをどのように使うかの感覚をおぼえましょう。

Functor クラスはすでに 5 章で議論しています。このクラ スは一つの演算 fmap のみを定義しています。このマップ関数はひと つの演算をコンテナ内のオブジェクトに適用し、(多相型は別の型の値のコ ンテナとして考えることが可能です) 同じ形のコンテナを返します。つぎの法則 は Functor クラスの map に適用されます。

fmap id = id
fmap (f . g) = fmap f . fmap g

これらの法則は、コンテナの形は map によって変更されないというこ とと、コンテナの中身がマップの演算によってならび変えられことはないという ことを保証するものです。

Monad クラスは 2 つの基本演算を定義しています。それは、 >>= (bind) と return です。

infixl 1  >>, >>=
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

    m >> k           =  m >>= \_ -> k

この bind 演算、>>>>= はふたつのモナド の値を合成し、return 演算は値をモナド(コンテナ)の中へ投入します。 >>= のシグネチャ Monad m => m a -> (a -> m b) -> m b をみれば、この演算を理解するのに役立ちます。 ma >>= \v -> mba 型の 値を含むモナドの値 ma と、型 a の値 v をとり モナドの値 mb を返す関数とを、合成します。その結果、mamb は合成されて、b 型の値を含むモナドの値となります。 >> は第二引数の関数が最初のモナド演算の結果生成されて値を 必要としない場合に用いられます。

bind の正確な意味は、もちろん、そのモナドに依存します。たとえば、IO モナ ドでは x >>= y はふたつのアクションを順に実行し ます。最初のアクションの結果はふたつめのアクションに渡されます。別の組み 込みのモナド、リストと Maybe について見てみましょう。これらのモ ナド演算は、ゼロ個あるいはそれ以上を値を一つの計算から次の計算へ渡すとい うことで理解できます。この例を簡単にみていきましょう。

do 構文を使うとモナド演算の鎖を簡単に表記できます。do の基本的な変換は以下のふたつのルールでとらえることができます。

  do e1 ; e2      =        e1 >> e2
  do p <- e1; e2  =        e1 >>= \p -> e2

do のふたつめの形式におけるパターンは反駁可能で、パターンマッチ が失敗すると fail 演算が呼ばれます。これが呼ばれると ( IO モナドのときと同様に)エラーが発生するか、あるいは、(リスト モナドのときと同様に)「zero」が返されます。次のものはもっと複雑な変換で す。

   do p <- e1; e2  =   e1 >>= (\v -> case v of p -> e2; _ -> fail "s")    

ここで、"s"do 文の位置を同定する文字列で、これは エラーメッセージのなかで使われるかもしれないものです。たとえば、I/O モナ ドのなかで、 'a' <- getChar のようなアクションは、 もし、型付けされた文字が 'a' ではなかった場合、fail を呼ぶでしょ う。こんどの場合はプログラムが停止します。なぜなら I/O モナドでは、 failerror を呼ぶからです。

>>= および return を支配している法則は以下の通り です。

return a >>= k = k a
m >>= return = m
xs >>= return . f = fmap f xs
m >>= (\x -> k x >>= h)= (m >>= k) >>= h

クラス MonadZerozero 要素および plus 演算を もつモナドに対して定義されています。

class  (Monad m) => MonadZero m  where
    mzero             :: m a
    mplus             :: m a -> m a -> m a

zero 要素は次の法則に従わなければなりません。

m >>= \x -> mzero = mzero
mzero >>= m = mzero

リストについていえば、zero 値は []、つまり空リストです。I/O モ ナドは zero 要素をもちません。つまり、このクラスには属しません。

mplus 演算を支配している法則は以下のようになっています。

m `mplus` mzero = m
mzero `mplus` m = m

この mplus 演算はリストモナドではふつうの連結演算です。

9.2  組み込みモナド

モナド演算とそれらを支配する法則があるとなにが構築することができるでしょ うか。I/O モナドについてはすでに詳しくしらべましたので、あとのふたつのモ ナドからはじめましょう。

リストのモナド bind はリスト中のそれぞれの値にたいしての一連の計算を結合 します。リストを使う場合は、>>= のシグネチャは以下のよう になります。

(>>=)                   :: [a] -> (a -> [b]) -> [b] 

これは、a のリストと abのリストへ写像する 関数が与えられたとき、bind が、この関数を、入力それぞれの a に 適用して、生成したすべての b のリストを一つのリストに連結します。 return 関数は単一要素リストを生成します。これらの演算については 既になれていることとおもいます。リストの内包表記はこのリストについて定義 されたモナド演算を使って簡単に表現することができます。以下の 3 つの式は すべて同じ式を別の構文で表現したものです。

[(x,y) | x <- [1,2,3] , y <- [1,2,3], x /= y]

do x <- [1,2,3]
   y <- [1,2,3]
   True <- return (x /= y)
   return (x,y)

[1,2,3] >>= (\ x -> [1,2,3] >>= (\y -> return (x/=y) >>=
   (\r -> case r of True -> return (x,y)
                    _    -> fail "")))

この定義は、このモナドでは fail の定義が空リストであることに依 存しています。基本的には、それぞれの <- は、のこりのモナド計 算へつぎつぎと渡される値の集合を生成します。ですから、 x <- [1,2,3] はのこりのモナド計算を 3 回起動するこ とになります。リストの各要素について 1 回ずつです。戻りの式 (x,y) はそれを取り巻く bind の可能な組み合わせすべてについて評 価されます。この意味ではリストモナドは多重値の引数をとる関数を記 述しているものと考えることができます。たとえば、つぎの関数を考えてみましょ う。

mvLift2                :: (a -> b -> c) -> [a] -> [b] -> [c]
mvLift2 f x y          =  do x' <- x
                              y' <- y
                              return (f x' y')

これは、通常の 2 引数関数を多重値(引数リスト)上の関数に変え ます。できた関数は 2 つの引数のすべての組み合わせのそれぞれに対して一つ の値を返します。たとえば、

mvLift2 (+) [1,3] [10,20,30] => [11,21,31,13,23,33]
mvLift2 (\a b->[a,b]) "ab" "cd" => ["ac","ad","bc","bd"]
mvLift2 (*) [1,2,4] [] => []

この関数はモナドライブラリの関数 LiftM2 の特別版です。これを、 関数 f をリストモナドの外から、多重値上の計算をおこなうリストモ ナドの世界へ持ち込んだものと考えることができます。

Maybe に対して定義されたモナドはリストモナドに類似しています。 値 Nothing[] の、Just x[x] のように機能します。

9.3  モナドを使う

モナド演算子とその法則について説明するだけではモナドがなんの役に立つのか わかりません。モナドがもたらす効用は、モジュール化 ( modularity )です。すなわち、演算をモナド的に定義することで、底 流にある計算機というものを隠蔽し、新しい特徴をモナドに透過的に結びつける ことを可能にします。 Wadler の論文 [10] はモ ナドを使っていかにモジュール化プログラムを構築するかの秀逸な例です。この 論文から直接、例をとるところからはじめましょう。状態モナドをもとによく似 た定義のさらに複雑なモナドを構築します。

簡単にいうと、状態の型 S についての状態モナドというのは次のよう なものです。

data SM a = SM (S -> (a,S))  -- The monadic type

instance Monad SM where
  -- defines state propagation
  SM c1 >>= fc2         =  SM (\s0 -> let (r,s1) = c1 s0 
                                          SM c2 = fc2 r in
                                         c2 s1)
  return k              =  SM (\s -> (k,s))

 -- extracts the state from the monad
readSM                  :: SM S
readSM                  =  SM (\s -> (s,s))

 -- extracts the state from the monad
updateSM                :: (S -> S) -> SM ()  -- alters the state
updateSM f              =  SM (\s -> ((), f s)) 

-- run a computation in the SM monad
runSM                   :: S -> SM a -> (a,S)
runSM s0 (SM c)         =  c s0

この例では新しい型 SM を定義して、型 S を暗黙に運ぶ計 算を表現しています。すなわち、SM t 型の計算は、型 t の値を定義し、一方で、型 S の状態と読み出しと書きこ みといった相互作用をします。SM の定義は単純で、一つの状態をとり、 2 つの結果を返す関数で構成されています。ひとつ(任意の型)は返り値で、もう ひとつは更新された状態です。ここでは型シノニムはつかえません。 SM という名前がインスタンス宣言で必要になるからです。こういった ところでnewtype 宣言を data 宣言のかわりによく使います。

このインスタンス宣言は、モナドの「配管」を定義しています。ふたつの 計算の直列化と空の計算の定義です。直列化( >>= 演算子)は、 (構築子 SM で表示されている)計算を定義しており、この計算は初期 状態 s0c1 へ渡し、この計算で得られ値 r を ふたつめの計算 c2 を返す関数に渡します。そして、最後に、 c1 で得られた状態を c2 に渡し、全体の結果を、計算 c2 の結果とします。

return の定義はもっと簡単です。return は状態をまったく 変更しません。値をモナドに入れ込むだけです。

>>=return は基本的なモナドの直列化演算ですが、 モナドプリミティブ( monadic primitives )も必要になります。モ ナドプリミティブはモナド抽象の中でもちいられる単なる演算子で、モナドが機 能するよう「車輪とギア」を接続します。 たとえば、IO モナドでは putChar のような演算子はプリミ ティブです。それは、これらの演算子は IO モナドの内部的な仕事を のみ取り扱うからです。 同様にして、状態モナドは 2 つのプリミティブ readSMupdateSM を使います。これらが、このモナドの内部構造に依存してい ることに注目してください。SM 型の定義を変更すると、これらのプリ ミティブも変更しなくてはなりません。

readSM および updateSM の定義は単純です。 readSM は状態を観察できるようモナドの外へ状態を持ち出します。一 方、updateSM はユーザがそのモナドの状態を変更することを許します。 ( writeSM をプリミティブとすることも可能なのですが、状態を扱う 場合には update の方がより自然な場合がおおいようです。)

最後に必要な関数はこのモナドの計算を駆動する関数 runSM です。こ の関数は初期状態と計算をとり、返り値と最終状態の両方を返す関数です。

より大きな視点でみると、やらんとしていることは、すべての計算を一連のステッ プ( SM a 型の関数)として定義し、>>=return を使って直列にならべることです。これらのステップは状態と ( readSM あるいは updateSM をつうじて)やりとりするか、 あるいは、状態を無視します。しかし、この状態の使用(不使用)は隠蔽されてい て、S を使用するしないに依存して、計算をべつべつに駆動あるいは 直列化することはありません。

この簡単な状態モナドの例をいくつも示すかわりに、状態モナドをつかったもっ と複雑な例に進みましょう。リソースを利用する計算のための、ちょっとした 埋め込み言語 ( embedded language ) を定義します。つまり、特 別な目的のための言語を Haskell の型と関数の集合として構築します。 このような言語は Haskell の基本ツールを使います。関数と型を使って、対象 分野に特化した型と演算ライブラリを構築します。

ここでの例では、なんらかのリソースを必要とする計算を考えましょう。もしリ ソースが利用可能であるばあい、計算が先へすすみ、リソースが利用できないと きには、計算が保留されます。これから定義するモナドによって制御されている リソースを使う計算を表示するために、型 R を使います。R の定義は次のようになります。

data R a = R (Resource -> (Resource, Either a (R a)))

おのおのの計算は、利用可能なリソースから、計算した後の残りにリソースと a 型の結果あるいは保留された計算とのペアへの関数です。その型は R a でこれはリソースが使い果されるところまで計算をつづけま す。

RMonad インスタンスは以下のようになります。

instance Monad R where
  R c1 >>= fc2          = R (\r -> case c1 r of
                                (r', Left v)    -> let R c2 = fc2 v in
                                                     c2 r'
                                (r', Right pc1) -> (r', Right (pc1 >>= fc2)))
  return v              = R (\r -> (r, (Left v)))

この Resource 型は状態モナドのなかの状態と同じ扱いになります。 この定義はこんな風に読みます。「リソースを消費する」ふたつの計算、 c1 と( c2 を生成する) fc2 を合成するためには、 初期リソースを c1 へ渡す。結果は、

です。計算の保留の際は、ふたつめの計算を考慮にいれておかなくてはいけません。 pc1 は最初の計算 c1 だけを保留しますので、計算全体を保 留するためには c2 をこれに bind しておく必要があります。 return の定義は、v をこのモナドへ投入するが、リソース を変更しないというものです。

このインスタンス宣言はこのモナドの基本構造を定義していますが、リソースが どのよに使われるのかは決められていません。このモナドは多くの型のリソース を制御するのに使うことが可能でおおくのタイプのリソース使用ポリシーを実装 することができるでしょう。例として、非常に単純なリソースの定義をあげましょ う。Resource として Integer を選択し、これを利用可能な 計算ステップ数を表現するものとします。

type Resource           =  Integer

次の関数は、利用可能なステップ数がなくなるまで、1ステップを消費します。

step                    :: a -> R a
step v                  =  c where
                              c = R (\r -> if r /= 0 then (r-1, Left v)
                                                     else (r, Right c))

LeftRight 構築子は Either 型のものです。 この関数は利用可能な計算ステップリソースが一つでもあれば、v を 返すことで R 内の計算を継続します。もし、利用可能なステップがな ければ、step 関数は現在の計算を保留し(この保留は c で捕捉されます)、この保留された計算をモナドへ戻します。

これで、一連の「リソースを用いる」計算(このモナド)を定義するツールを手に 入れましたので、step を用いてリソースの使用状況の形式を表現する こができます。最後に、このモナドでは計算はどのように表現されるのかに言及 しておく必要があります。

このモナド内でインクリメントを行う関数を考えてみましょう。

inc                     :: R Integer -> R Integer
inc i                   =  do iValue <- i
                              step (iValue+1)

これは、計算の1ステップをインクリメントする関数を定義しています。 <- はこのモナドから引数の値を引出すのに必要なものです。 iValue の型は R Integer ではなくて Integer です。

しかし、この定義は一般のインクリメント関数に比べると満足のいくものではあ りません。なんとか、既存の + 演算子を「ドレスアップ」して、この モナドの世界で機能するようにできないものでしょうか。持ち上げ ( lifting )をおこなう関数の集りから始めましょう。これらの関数は、 既存の機能をモナド内へ持ち込みます。 lift1 の定義(これはMonad ライブラリの中の liftM1 とは少し違うものです)を考察してみましょう。

lift1                   :: (a -> b) -> (R a -> R b)
lift1 f                 =  \ra1 -> do a1 <- ra1
                                      step (f a1)

この関数は単一引数関数 f をとり、R 内で1ステップ消費し て持ち上げられた関数を実行する関数です。この lift1 を用いると inc は次のようになります。

inc                     :: R Integer -> R Integer
inc i                   =  lift1 (i+1)

これで、ましにはなりましたが、理想的というわけにはいきません。まず、次の lift2 を加えてみましょう。

lift2                   :: (a -> b -> c) -> (R a -> R b -> R c)
lift2 f                 =  \ra1 ra2 -> do a1 <- ra1
                                          a2 <- ra2
                                          step (f a1 a2)

この関数は、持ち上げられた関数内での評価の順番を明示的に設定していること に注目してください。a1 を生成する計算は、a2 を生成する 計算より前におこなわれます。

lift2を使うと R モナド内で新しいバージョンの == を生成することができます。

(==*)                   :: Ord a => R a -> R a -> R Bool
(==*)                   =  lift2 (==)

この新しい関数については少しちがう名前をつかわなければなりませんでした。 それは == はすでに使われていますから当然です。しかし、場合によっ ては、持ち上げ前の関数と持ち上げ後の関数とでおなじ名前を用いることができ ます。 以下のインスタンス宣言は、Num のすべての演算子が R な いで使えるようにするものです。

instance Num a => Num (R a) where
  (+)                   =  lift2 (+)
  (-)                   =  lift2 (-)
  negate                =  lift1 negate
  (*)                   =  lift2 (*)
  abs                   =  lift1 abs
  fromInteger           =  return . fromInteger

Haskell プログラムでは、fromInteger 関数が、すべての整数定数に ついて暗黙のうちに適用されます。(これについては、10.3 を参照してください。) こ の定義のおかげで整数定数は型 R Integer となることがでます。 やっとこれで、自然で完全なインクリメント関数を書くことができます。

inc                     :: R Integer -> R Integer
inc x                   =  x + 1

Eq クラスは Num クラスと同じやりかたで持ち上げることは できないことに注意してください。==* のシグネチャは == の多重定義でなんとかなるようなものではありません。それは ==* の 結果が Bool ではなくて、R Bool であるからです。

R 中で興味のわく計算を表現するためには、条件式が必要です。 if は使えませんから( if は条件式として、 R Bool ではなくて Bool 型が必要です。)、 ifR という名前の関数を考えます。

ifR                     :: R Bool -> R a -> R a -> R a
ifR tst thn els         =  do t <- tst
                              if t then thn else els

これで、R モナドのなかで大きなプログラムを書く準備がととのいま した。

fact                    :: R Integer -> R Integer
fact x                  =  ifR (x ==* 0) 1 (x * fact (x-1))

さて、次は一般の階乗関数と全く同じものというわけにはではありませんが、十 分に読みやすくなっています。+ あるいは if のような既存 の演算子に新しい定義を与えると、Haskell のなかの埋め込み言語の基本的な部 分を作ることにほかなりません。モナドは特に、これらの埋め込み言語の意味論 をクリーンでモジュール性のある方法でカプセル化するのに便利です。

これで、なにかプログラムを走らせる準備ができました。以下の関数は、プログ ラムを、最大計算ステップ数をあたえられた M の中で動作させるもの です。

run                     :: Resource -> R a -> Maybe a
run s (R p)             =  case (p s) of 
                             (_, Left v) -> Just v
                             _           -> Nothing

割当てられたステップ数ないに計算が終了するかどうかを Maybe 型を 利用して扱います。それで、次のような計算が可能になります。

run 10 (fact 2) => Just 2
run 10 (fact 20) => Nothing

ここにきて、このモナドにいくつかの興味深い機能を加えることができます。次 の関数を考えてみましょう。

(|||)                   :: R a -> R a -> R a

この関数は 2 つの計算を並列に実行し、最初に終った方の結果を返します。こ の関数のは例えば、こんな風に定義できるでしょう。

c1 ||| c2                =  oneStep c1 (\c1' -> c2 ||| c1')
   where
        oneStep          :: R a -> (R a -> R a) -> R a
        oneStep (R c1) f =
             R (\r -> case c1 1 of
                         (r', Left v) -> (r+r'-1, Left v)
                         (r', Right c1') ->  -- r' must be 0
                          let R next = f c1' in
                            next (r+r'-1))

これは、c1 で 1 ステップで、c1 が終了すればその 値を返すか、または、c1 が保留された計算( c1' )を返した ときには、c2 ||| c1' を評価する、のどちらかです。 この oneStep 関数はその引数内で単一ステップを消費し、評価された 値を返すか、あるいは、のこりの計算を f に渡すかのどちらかです。 oneStep の定義は単純です。これは c1 にリソース引数とし て 1 を与えます。もし、それで最終の値に到達したら、その値が返ります。返 されるステップカウント値(計算がステップを消費せずに戻ることはありえます。 ですからこのときリソースカウントが 0 である必要はありません)も調整されて います。この計算が保留された場合、調整されたリソースカウントが最後の計算 に渡されます。

これで、run 100 (fact (-1) ||| (fact 3)) のような式をループに陥ることなしで評価することができます。計算が相 互に実行されるからです。(先に定義した fact では -1 を 与えるとループに陥ります。 この基本構造の上にいろいろなバリエーションを考えることができます。たとえ ば、状態を拡張して計算のステップのトレースを含むようにすることも可能です。 このモナドを IO モナドへ埋め込むことも可能です。そうすれば、 M 内の計算は外界とやりとりすることが可能になります。 ここでの例はこのチュートリアルの他の部分より高度なものですが、システムの 基本的な意味論を定義する道具としてのモナドの力をよく説明しています。また、 この例を小規模の領域限定言語( Domain Specific Language ) のモデルとなることを説明しました。あるいは、Haskell がこうしたものを定義 するのに適しているということも示しました。 おおくの DSL が Haskell で開発されています。おおくの例が、 haskell.org にありますので参照してください。特に興味ある例は、 リアクションのあるアニメーションのための言語 Fran とコンピュータミュージッ クのための言語 Haskore です。


A Gentle Introduction to Haskell, Version 98
back next top