Stateモナドがわからない状態から、ギリギリ分かる状態になった。
Stateモナドを学習した流れ 結局、具体例を通して学習した。個人的には、いきなりモナドの定義から学習するよりも、たくさんの例を見たり、実際に例を作ってみたりした方が覚えられた。抽象的な概念を理解するためには具体的な概念に触れるべきだ、ということを改めて認識した。
以下は、自分が行った学習の流れ。Haskell IOモナド 超入門は学習のうえで参考になった。とくに、>>=を漏斗の形に見立てる比喩のおかげで、モナドと関数の組み合わせのイメージがクリアになった。
Maybeモナド、Listモナドの使い方を理解する。 IOモナドの使い方を理解する。 いくつかのモナドについて、do構文を>>=に書き換えてみる。 Stateモナドの使い方を理解する。 Stateモナドを自作する。 この記事ではStateモナドを自作することをテーマとしているため、ある程度Stateモナドに慣れた人でないとわかりづらいかもしれない。
Stateの定義 まずはStateを自作する。Stateは、状態 -> (計算結果,次の状態)という関数を内部に持っている。この関数のことを、この記事では「内部関数」「状態付き計算」などと表現する。
1 newtype State s a = State (s -> (a, s)) これは本来のStateの定義とは異なることに注意。本来は、StateはStateTを使って実装されている。上のように定義してしまうと、モナド変換子としての機能が利用できない。ただ、そこまで考えると面倒なので、今回はStateを単なる関数のラッパーとして定義した。
型引数の順番と内部関数が返すタプルの順番が逆なのが微妙に気持ち悪い。これはあくまで推測でしかないが、
あくまで状態付きの計算なので、重要なのは計算の結果。なので返り値は(a, s)と計算結果を先に書いている。 型引数の順番がs aなのは、Monadにするときに不都合を生じないため。 なのだと思う。
余談 Stateモナドがよくわかっていない時は、Stateのことを「状態を持つ型」と勘違いしていた。正しくは、「状態付き計算を持つ型」。Stateは状態を持っているわけでなく、あくまで、「状態を引数にとり、計算結果と次の状態を返す関数」を持っている。なので、初期状態は内部関数の引数として、自分で投入する。
runStateの定義 レコード構文を使って、runStateを定義する。runStateは、Stateから中身の関数を取り出す関数。
1 newtype State s a = State { runState :: s -> (a, s) } 試す 上の定義を踏まえて、次のようにプログラムを書いてみる。以下は、状態を[Int]とする状態付き計算。
addX doubleAll sumUpはそれぞれ、単純な内部関数を持つStateである。一方で、calc0はこれらの関数を組み合わせた、新たなStateであることに注目。一連の状態付き計算を一つにまとめて、新たな状態付き計算を作っている。
calc0において、初期状態をs、次の状態をs0、その次の状態をs1、…と置いている。計算結果を返すのはsumUpだけで、他の関数は単に状態を変更するだけ。なので計算結果は()となっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 addX :: Int -> State [Int] () addX x = State $ \s -> ((), x:s) doubleAll :: State [Int] () doubleAll = State $ \s -> ((), map (* 2) s) sumUp :: State [Int] Int sumUp = State $ \s -> (sum s, s) calc0 :: State [Int] Int calc0 = State $ \s -> let (_, s0) = runState (addX 1) s (_, s1) = runState (addX 2) s0 (a2, s2) = runState sumUp s1 (_, s3) = runState (addX a2) s2 (_, s4) = runState doubleAll s3 in runState sumUp s4 main = do print $ runState calc0 [] 実行結果は以下のようになる。...