つまずきの記憶 - モナド(2)

'15年2月12日
タグ Haskell

前回の記事では、モナドの舞台裏となる枠組み(文脈)、言い換えれば ma >> mb としたときに ma から mb へ伝わるものについて考えてみた。もう少し続けてみようと思うが、その前に、モナドにするためのインスタンス宣言を確認しておく。

instance Monad 型構築子 where
    ・・・

型構築子は Maybe a[a] のように引数をひとつだけ取るものである。メモ化の記事で登場した Mc という型構築子は引数をふたつ必要とするもので、モナドのインスタンスとする場合は部分適用を行って instance Monad (Mc s) where としたのであった。このあたりのことは、「ゾウ本」のp.154の「EitherはFunctorであるか否か」という節で説明されており、モナドの場合も同様に考えて良い。Either については後に触れることにする。

で何が言いたいのかと言うと、インスタンス宣言の Monad の後の型構築子の部分がモナドの舞台裏の「枠組み」に相当し、型構築子が必要とする型引数がモナドの「値」となる、ということである。例えば Maybe a の場合は、(NothingJust で構成される)Maybe が枠組みで、a の部分が値となる。「そんなのアッタリマエだろ」と言われそうであるが、最初の頃はそんなことも良く分かっていなかった。頭が悪いとツライものである。

引数が2個以上の型構築子

いま触れたように、モナドのインスタンスにできるのは型引数がちょうど1個の型構築子なので、2個以上の引数をとる型構築子の場合は部分適用を行って引数を1個にする必要がある(モナドにできるかどうかはまた別の話)。

引数を2個とる型構築子 Either は以下のようにしてモナドのインスタンスとなっている:

data Either a b = Left a | Right b  -- Either の定義

instance Monad (Either a) where
    return = Right
    Left  l >>= _ = Left l
    Right r >>= k = k r

Either の定義から deriving 以下は省略してある)

Either のひとつめの引数は Left 値となるものであり、インスタンス宣言において (Either a) が「枠組み」とされているので、Left 値は枠組みの構成要員であり、モナドとしての「値」は Right 値のみとなる。

(>>) のデフォルトの定義は、m a >> m b = m a >>= (\_ -> m b) であった。これを Either での (>>=) の定義にあてはめてみると、

    Left  l >> _ = Left l
    Right r >> e = e

となる。つまり、Maybe a 型のデータを >> で連結した場合、途中に Nothing が現れると結果も Nothing となったように、Either a b 型のデータを >> で連結した場合は、途中にLeft値が現れたら結果もLeft値となる。さらに、Left x >> Left y = Left x となることより、一番最初に(左側に)現れたLeft値が結果となる。Right値のみを >> で連結した場合は結果もRight値となるが、>> の左辺の値は捨てられるので、一番最後に(右側に)現れたRight値が結果となる。したがって、>> の背後で伝わっていくものはLeft か Right かだけではなく、Left の場合は伴っている値も伝わっていく。

何かの処理結果に Either 型を使い、処理が正常に終わった場合は Right b で結果bを返し、異常終了の場合には Left a で異常の内容aを返すものとする。Either a b 型の結果を返す処理 f1, f2, … を順次行った場合、f1 >> f2 >> ... >> fn という式は、全ての処理が正常に行われた場合は最後の fn の結果が Right y で返され、ひとつでも異常があった場合は一番最初に発生した異常が Left x で返される。

モナドの「値」

これまでは主にモナドの背後で働いているものについて考えてきたが、ここで少しモナドの「値」について考えてみる。

まず、以下の内容のファイルを作り、GHCiで読み込んでみる。足し算をするので、型引数の aNum クラスに属するものでなければいけないが、モナドは何でも良い。

-- モナド値の足し算をする。
madd :: (Monad m, Num a) => m a -> m a -> m a
madd ma mb = do
    a <- ma
    b <- mb
    return (a + b)

読み込んだら、以下のように madd に色々な引数を与えて結果を眺めてみる。

*Main> madd (Just 1) (Just 2)
Just 3
*Main> madd (Just 1) Nothing
Nothing
*Main> madd Nothing (Just 2)
Nothing

Maybe モナドの例はどの本にも出ていることなので特にどうということはない。ただ、do 構文を使うとモナドの枠組みが表に現れてこないので、ボクのように「なんとなく分かったような気になったけど、実は良く分かっていなかった」ということになりかねない。

本当に分かっているかどうかは、以下の質問に即答できるかどうかで判定できる:

ma = Just 1 の場合、a <- maa の値は 1 となる。
では、ma = Nothing の場合 a は何になるか?

madd でやっていることは、モナド ma, mb からそれぞれ値 a, b をとってきて、その後の処理(足し算)に渡して結果を得ている、ということである。これは、モナドのbind演算子 (>>=) がやっていることであり、do はその構文糖衣である。したがって、madd>>= を用いて以下のように書き換えられる:

madd ma mb = ma >>= (\a -> mb >>= (\b -> return (a + b)))

文法的にはラムダ式を囲むカッコは不要だが、>>= の引数となる関数を明示するためにカッコをつけた。あるいは、元の表記に近い次のような書き方を好む人も多いと思う:

madd ma mb = ma >>= \a ->
             mb >>= \b ->
             return (a + b)

どちらの書き方でも意味は同じである。ここで、Maybe における (>>=) の定義を思い出してみると、ma = Nothing の場合は第2引数となる関数は無視されて Nothing となるのであった。したがって、先の質問の「a は何になるか?」の答えは「何にもならない」である。ma = Nothing の時点で do 全体が Nothing となり、a が何かの値に束縛されることも無いのであるが、do 構文を使うとそのような事情(枠組み)が背後に隠れてしまって見えない。枠組みをきちんと理解していないと、do 構文のコードをいくら眺めても舞台裏で行われていることまでは分からないので、「どうも良く分からない」という状態に陥ってしまう。

そうならないように、「RWH本」は「モナド初心者はdo構文ではなく明示的に >>= を使うべし」と忠告(p.346)してくれているのだと思うのだが、ちゃんと言うことを聞かなかったので理解までにずいぶん遠回りをしたような気がする。

maddEither モナドを使ったときの挙動は Maybe の場合の NothingLeft x と考えれば、だいたい予想できる。

*Main> madd (Left 1) (Left 2)
Left 1
*Main> madd (Left 1) (Right 2)
Left 1
*Main> madd (Right 1) (Left 2)
Left 2
*Main> madd (Right 1) (Right 2)
Right 3

リストモナドの実験で今回の記事を終わりにしたい。

*Main> madd [1] [10]
[11]
*Main> madd [1] []
[]
*Main> madd [] [10]
[]
*Main> madd [1,2] [10,20]
????

最後の????が何になるか即答できなければまだ分かっていないということである。

次回はStateモナドを題材として、引き続きモナドの「枠組み」と「値」について書こうと思うが、先に Functor や Applicative に触れたほうが良いかとも思っている。つまずいていた頃の自分に理解してもらえるような文章を書くのはなかなか難しいものである。