モナドを自分なりに整理してみる

July 27, 2015

ここ数年間haskellを勉強しているのですが、モナドがやっぱりまだわかりにくいので自分なりに整理してみます。あっているのかどうかは自信がありませんのであしからず・・・。ちなみに参考図書は、「すごいHaskellたのしく学ぼう!」です。

前提となる知識はファンクター、アプリカティブファンクターですので復習を軽くしてみます。

  • ファンクターとは?

ファンクターは関数で全体を写せる型クラスで下記のように定義されています。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

fという型変数は1つの型引数を取る型コンストラクタ(例えばMaybe、[]など)です。上記の定義からfmapは、「ある型aから別の型bへの関数」と、「ある型aに適用されたファンクター値」を取り、「別の型bのほうに適用されたファンクター値」を返す関数であることがわかります。mapはリストの中の各値を別の型(同じでもよい)の値に変換してリストとして返す関数でしたが、fmap関数はリストだけに限定せずに任意の型コンストラクタに対して適用できるより一般化されたmap関数である、といえそうです。

Maybeもファンクターのインスタンスで下記のように定義されています。

instance Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap f Nothing = Nothing

例えば、

fmap (*2) (Just 200)

はJust 400を返します。

  • アプリカティブファンクターとは?

アプリカティブファンクターはファンクターの強化版で、「ファンクターの中の関数」で「別のファンクターの中の値」を写すことができます。型クラスの定義は下記のようになっています。

class (Functor f) => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

pureは値をとって内部にその値を結果として含むアプリカティブ値にくるみます。正確には、値をとって、その値を再現できるような最小のデフォルト文脈に入れます。普通の関数をアプリカティブ値にしたい時に使われるようです。

<*>は関数の入っているファンクター値の中の値と値の入っているファンクター値を引数にとって、1つ目のファンクターの中身である関数を2つ目のファンクターの中身に適用します。

Maybeもアプリカティブファンクターのインスタンスで下記のように定義されています。

instance Applicative Maybe where
    pure = Just
    Nothing <*> _ = Nothing
   (Just f) <*> something = fmap f something

アプリカティブファンクターは1つの関数で複数のファンクターを連続して写すことができます。例えば

pure (+) <*> Just 3 <*> Just 10

はJust 13を返しますし、

pure (++) Just "exdeath" <*> Nothing

はNothingを返します。

アプリカティブ値は「文脈の付加された値」だと見なせます。そして、Applicative型クラスはこれら文脈のついた値に文脈を保ったまま普通の関数を適用させてます。例えばMaybe Char型の値は文字かもしれないし、文字がないことを表す(Nothing)のかもしれないという文脈が付加されています。普通の関数は文脈を考慮しませんが、アプリカティブ値にくるんで<*>で適用することにより、文脈を保ったまま関数の部分適用ができるようになる、といえると思います。

  • 本題のモナド

モナドはアプリカティブ値を拡張したもので、「文脈付きの値m aを、普通の値aをとって文脈付きの値を返す関数(a -> mb)に渡す」ことができる「>>=」(バインドと呼ばれる)という関数を持っています。

モナド型クラスの定義は下記のようになります。returnはアプリカティブクラスのpureと同じで、値をとって、その値を再現できるような最小のデフォルト文脈にいれます。>>は文脈を考慮しつつ規定のモナド値を返す関数で、デフォルト実装が定義されています。

class Monad m where
    return :: a -> m a

    (>>=) :: m a -> (a ->m b) -> m b

    (>>) :: m a -> m b -> m b
    x >> y = x >>= \_ -> y

    fail :: String -> m a
    fail msg = error msg

Maybeもモナドインスタンスで、下記のように定義されています。

instance Monad Maybe where
    return x = Just x
    Nothing >>= f = Nothing
    Just x >>= f  = f x
    fail _ = Nothing

モナドの>>=もアプリカティブファンクターの<*>と同じように連続して適用できますし、適用によって文脈が保存されるところもいっしょです。では何が違うかというと、アプリカティブファンクターは関数をくるんだアプリカティブ値に対し複数のアプリカティブ値を適用することで、文脈を保存しながら関数の部分適用を連続して行えただけですが、モナドの場合は、連続して複数の異なる関数を文脈を保存しながら適用できる点が異なるのだと思います。

例えば、与えられた整数に対して10を足し、もし50以下なら結果をJustにくるんで返し、そうでないならNothingを返す(失敗したことを表す)ような関数plus10を考えてみます。

plus10 :: Int -> Maybe Int
plus10 x
  | ans > 50 = Nothing
  | otherwise = Just ans
  where ans = x + 10

この関数を>>=で連続適用してみます。

return 30 >>= plus10 >>= plus10

の結果はMaybe 50になりますが、さらにplus10を適用し続けると、ずっとNothingになります。つまり、50を越えた時点で失敗になり、それ以降はずっと失敗するというわけです。

今度は逆に、与えられた整数に対して10を引き、もし-50以上なら結果をJustにくるんで返し、そうでないならNothingを返すような関数minus10を定義し、plus10と組み合わせて使ってみます。

minus10 :: Int -> Maybe Int
minus10 x
  | ans < -50 = Nothing
  | otherwise = Just ans
  where ans = x - 10

さて、

return 40 >>= plus10 >>= plus10 >>= minus10

の結果はどうなるでしょうか。40に10を2回足して、10を1回引くのだから、答えはMaybe 50かというとそうはならず、Nothingになります。10を2回足した時点でNothingになってしまうので、それ以降の関数の適用ではずっとNothingになってしまうからです。つまり、文脈が保存しながら関数を適用している、ということを示しています。

このように、モナドは文脈を保存しながら複数関数を連続して適用したいときに使えます。

  • リストモナド

Maybeの場合は文脈は「正しい結果」か「失敗」か、を表していましたが、他のモナドの場合の文脈も調べてみましょう。ここではリストのモナドインスタンスを取り上げてみます。

instance Monad [] where
  return x = [x]
  xs >>= f = concat (map f xs)
  fail _ = []

モナドクラスの定義のところで、>>=の引数である関数fのところは(a->m b)と定義されていました。リストの場合、mは[]ですので、fは(a->[b])、つまり型aの値をとって、型bの値のリストを返す関数であることになります。concatはリストのリストをとって、各リストを結合して生成するリストを返す関数です。このことから

[3,4,5] >>= \x -> [x, -x]

の結果は[3,-3,4,-4,5,-5]になることがわかると思います。3,4,5それぞれに対して正、負の値を求め、リストとして出力し、各リストを1つのリストに結合しているわけです。

このリスト値の文脈は「今どんな値をとりうるのかを列挙したもの」、とみなすことができます。>>=を連続適用することでとりうる値はどんどん変化していくわけです(リストが短くなる場合もある)。

空リストもリスト値ですが、これは「どの値も取りえない」という文脈です。そのような状態になると、>>=を適用してもそれ以降はずっと結果は空になります。例えば、

[] >>= \x -> [x, -x]

は[]です。Maybeと同じく、文脈が保存されていることがわかります。

  • do記法

do記法はモナド専用の糖衣構文です。do記法を使うことで、>>=を入れ子になったラムダ式で連続適用したい場合に、ラムダ式の記述を簡略化することができます。例えば

testfunc :: Maybe Bool
testfunc = Just 9 >>= (\x -> Just 7 >>= (\y -> Just (x + y > 8)))

testfunc :: Maybe Bool
testfunc = do
  x <- Just 9
  y <- Just 7
  Just (x + y > 8)

と書けます。最初の>>=ではxに9が束縛されますが、次の>>=は入れ子になっているので、ラムダ式の中でxが使えるのです。いくつかの変数に値を束縛した後に、なんらかの結果を求めるスタイルであるという点でlet式に似ていますが、実際は入れ子になった>>=の連続適用であるので、常に文脈は保存され、どこかで失敗になればそれ以降はずっと失敗になります。

do式の各行が1つの>>=に相当しますが、値を束縛する必要がない場合は<-を省略することができます。もちろん、値を束縛しなくても文脈は保存されます。下記の例では途中で>>=にNothingが渡されるため、以降はNothingとなります。

testfunc :: Maybe Bool
testfunc = do
  Just 9
  Nothing
  x <- Just 10
  Just (x + 4 > 11)

また、do式の中でモナド値から中の値を変数に束縛するのではなく、純粋な値(非モナド値)を変数に束縛したい場合はlet構文が使えます。下記の例ではa,bに整数値を束縛して最後の行で参照しています。

testfunc:: Maybe Bool
testfunc = do
  x <- Just 9
  let a = 100 * x
        b = 200 + x
  Just (a + b + x > 300)

do式の結果は(最終行の)モナド値となりますので、do式の中でdo式を使用することもできます。

testfunc :: Maybe Bool
testfunc = do
  x <- Just 9
  y <- do
    z <- Just 10
    Just (z + 5)
  Just (x + y > 30)