読者です 読者をやめる 読者になる 読者になる

Haskell Stateモナド(状態モナド遊びについての理解)

状態モナド遊び - あどけない話

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

HaskellのStateモナドは関数を扱うため、Maybeなどのモナドに比べてさらに理解が難しい。上記のサイトや参考書はそれをうまく説明しているが、それでも直感的にはまだ理解しづらいので、上記サイトの内容を以下のように紐解いてみるとより理解が深まると思う。

まず、Monadicモナドとcons0、runの定義は以下である。

instance Monad Monadic where
    Monadic f >>= g = Monadic $ \cnt0 -> let (result1, cnt1) = f cnt0
                                             Monadic h       = g result1
                                             (result2, cnt2) = h cnt1
                                         in (result2, cnt2)
    return x = Monadic $ \cnt -> (x, cnt)

cons0 x xs = Monadic $ \cnt -> (Cons x xs, cnt + 1)

run (Monadic func) initCnt = func initCnt

求めたい式は以下である。

run (cons0 "a" Nil >>= cons0 "b" >>= cons0 "c") 0

ここで、cons0 "a" Nilの部分を、その定義通りに置き換えてみる。

run (Monadic $ \cnt -> (Cons "a" Nil, cnt + 1) >>= cons0 "b" >> = cons0 "c") 0

最初の>>=を、その定義通りに置き換えてみる。

run (
    Monadic $ \cnt0 -> let (result1, cnt1) = \cnt -> (Cons "a" Nil, cnt + 1) cnt0
                            Monadic h      = cons0 "b" result1
                           (result2, cnt2) = h cnt1
                       in  (result2, cnt2)
    >>= cons0 "c"
) 0

cons0 "b" result1の部分も、その定義通りに置き換えてみる。

run (
    Monadic $ \cnt0 -> let (result1, cnt1) = \cnt -> (Cons "a" Nil, cnt + 1) cnt0
                            Monadic h      = Monadic $ \cnt -> (Cons "b" result1, cnt + 1)
                           (result2, cnt2) = h cnt1
                       in  (result2, cnt2)
    >>= cons0 "c"
) 0

ここで一度、置き換えた結果出現した関数の部分をfとおく。

run (
    Monadic $ f >>= cons0 "c"
) 0

次の>>=を、その定義通りに置き換えてみる。

run (
    Monadic $ \cnt0 -> let (result1, cnt1) = f cnt0
                            Monadic h      = cons0 "c" result1
                           (result2, cnt2) = h cnt1
                       in  (result2, cnt2)
) 0

cons0 "c" result1の部分も、その定義通りに置き換えてみる。

run (
    Monadic $ \cnt0 -> let (result1, cnt1) = f cnt0
                            Monadic h      = Monadic $ \cnt -> (Cons "c" result1, cnt + 1)
                           (result2, cnt2) = h cnt1
                       in  (result2, cnt2)
) 0

runを、その定義通りに置き換えてみる。

\cnt0 -> let (result1, cnt1) = f cnt0
              Monadic h      = Monadic $ \cnt -> (Cons "c" result1, cnt + 1)
             (result2, cnt2) = h cnt1
         in  (result2, cnt2)
0

つまり、runに与えた初期値0は、ラムダ式の引数cnt0に渡るので、それがそのままfに渡ることになる。これは、>>=をいくつ連結していても同じことである。fがネストされている状態になり、cnt0の値はfを通る度に規定された状態変化(この場合はcnt + 1)を伴いながら伝搬していく。

ここで再度fを見てみる。上記で見たとおり、cnt0は初期値0である。

Monadic $ \cnt0 -> let (result1, cnt1) = \cnt -> (Cons "a" Nil, cnt + 1) cnt0
                        Monadic h      = Monadic $ \cnt -> (Cons "b" result1, cnt + 1)
                       (result2, cnt2) = h cnt1
                   in  (result2, cnt2)

cnt0=0を代入して計算すると、fの結果は(Cons "b" (Cons "a" Nil), 2)となることが分かる。

ここで以下のincr1を使った例についても考えてみる。

incr1 = Monadic $ \cnt -> (999, cnt + 1)

cons1 x xs = do incr1
                return (Cons x xs)

run (cons1 "c" =<< cons1 "b" =<< cons1 "a" Nil) 0

doは構文糖衣で、>>=を使った式に書き換えられる。ただし、ここでincr1の結果を使っていないので、これは引数を取らないラムダ式になる。

cons1 x xs = incr1 >>= \_ -> return (Cons x xs)

incr1とreturnを、その定義通りに置き換える。

Monadic $ \cnt -> (999, cnt + 1) >>= \_ -> Monadic $ \cnt -> (Cons x xs, cnt)

>>=を、その定義通りに置き換える。すると、ラムダ式の引数に渡されたresult1の結果(ここでは999)は使われないことが良く分かる。

Monadic $ \cnt0 -> let (result1, cnt1) = \cnt -> (999, cnt + 1) cnt0
                       Monadic h       = \_ -> Monadic $ \cnt -> (Cons x xs, cnt) result1
                       (result2, cnt2) = h cnt1
                   in (result2, cnt2)

先ほどのcons0の場合と形は変われど、cons1の場合も同様に>>=の結合になるので、runに初期値として与えられたcnt0は、状態変化を伴いながら伝搬していくことになる。 incr2の例は、状態から取り出した値であるresult1の結果がラムダ式に渡ることになる。

ポイントになったのはcons1 x xsに現れたincr1だと思う。命令型脳で考えていると、このincr1のラムダ式が意味を成さない。また、構文糖衣であるdo式は命令型言語のように式を書ける便利構文である、と単純化してしまっていて、本来は>>=であることをスキップしてしまっている。そして、Haskellが純粋関数型であり、遅延評価を行い、>>=がbindと呼ばれる糊付け役である、ということを常に意識しながら読むことが大事である。