Haskell Stateモナド(状態モナド遊びについての理解)
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (70件) を見る
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と呼ばれる糊付け役である、ということを常に意識しながら読むことが大事である。