Haskell: Control.Monad.Stateメモ
機能一覧や例は以下にあるけど、これだけで理解するのは正直難しい。
理解しにくそうなところを意識して書いてみる。
なにをするものか:
関数の中で上から順番にデータを更新していく処理を書きたいとき使う。だけではあるけど。
まずはimport Control.Monad.Stateを行ってState型を使い、
State stateType resultTypeという型で状態更新を利用するstateApp関数を自由に実装する(ドキュメントの例は戻り値型が同じで紛らわしいので型をStringに変えてます。$はあえてすべて()にしてます):
import Control.Monad.State tick :: State Int String tick = do n <- get put (n + 1) return (show n) ticktick :: State Int [String] ticktick = do s1 <- tick s2 <- tick n <- get return ([s1] ++ [s2] ++ [show n])
tickやticktickはstateAppです。この関数はMonad型になっているのでdo記法が使え、さらにgetで状態取得、putで状態更新が使える。内側ではstateAppが使え、<-によってresultTypeが得られる。
なぜState型にresultTypeがいるかというと、それは関数型言語での普通の関数のように使うため。ticktickの中で、tickは状態を更新するだけでなく、Stringを返す関数のように扱っている。
しかし、直接はこのtickやticktickは使えない
*Main> ticktick Top level: No instance for (Show (State Int [String])) arising from use of `print' at Top level Probable fix: add an instance declaration for (Show (State Int [String])) In a 'do' expression: print it *Main>
stateApp関数を使うには、もっとも上で初期状態を与えるrunState経由で実行する必要がある。
*Main> runState ticktick 3 Loading package mtl-1.0 ... linking ... done. (["3","4","5"],5)
runStateの代わりに、execStateにすると、最終状態の5だけになり、evalStateで実行すると結果の["3","4","5"]だけになる。
*Main> evalState ticktick 3 ["3","4","5"] *Main> execState ticktick 3 5
外側でも、最終状態を次のrunStateに渡せば繰り返してるようになる
*Main> let (r1, s1) = runState ticktick 3 in runState ticktick s1 (["5","6","7"],7)
stateは、結局大域変数を保存するわけではなく、runStateの範囲内で単一のgetで取れる値を更新し続けるもの、ということになる。
IOとの組み合わせ
内部でIOを使う場合は、StateではなくStateTを使うといい
import Control.Monad.State tick :: StateT Int IO String tick = do n <- get put (n + 1) return (show n) ticktick :: StateT Int IO [String] ticktick = do s1 <- tick lift (putStrLn s1) s2 <- tick lift (putStrLn s2) n <- get return ([s1] ++ [s2] ++ [show n])
IOを使うところはlift (Control.Monad.Transにある)で型を調整し、State処理の順番で呼び出せる。
*Main> runStateT ticktick 3 Loading package mtl-1.0 ... linking ... done. 3 4 *Main>
runStateT ticitick 3の型はIO ([String], Int)であり、外側でIOのdoの中から呼び出せる
loop s0 = do putStr "> " line <- getLine case line of "q" -> return () otherwise -> do (r1, s1) <- runStateT ticktick s0 loop s1
戻ってきた状態を再帰的に(最終的にはrunStateTに)渡すことで状態更新続けるようなコードになる。
実行してみると
*Main> loop 3 Loading package mtl-1.0 ... linking ... done. > a 3 4 > a 5 6 > a 7 8 > q *Main>
以下のようにliftでIOから値を受けることもできます。
ticktick :: StateT Int IO [String] ticktick = do s1 <- tick lift (putStrLn s1) lift (putStrLn "hit any") line <- lift getLine lift (putStrLn ("[" ++ line ++ "]")) s2 <- tick lift (putStrLn s2) n <- get return ([s1] ++ [s2] ++ [show n])
このliftはdoでまとめられます。
ticktick :: StateT Int IO [String] ticktick = do s1 <- tick lift (putStrLn s1) lift (do putStrLn "hit any" line <- getLine putStrLn ("[" ++ line ++ "]")) s2 <- tick lift (putStrLn s2) n <- get return ([s1] ++ [s2] ++ [show n])
状態に独自型を使う
Stateにはレコード型も使える。それでもIntと同様、putは状態全体を更新する。
独自型を状態にする場合、メンバーなど一部を更新することのほうが多くなる。その場合はmodifyを使う
data Prof = Prof {name :: String, age :: Int } deriving Show incAge :: State Prof Int incAge = do prof <- get let newAge = (1 + age prof) modify (\p -> p {age = newAge}) return newAge
runStateさせてみる
*Main> runState incAge Prof{name="Taro", age=10} Loading package mtl-1.0 ... linking ... done. (11,Prof {name = "Taro", age = 11}) *Main>
HaskellのStateモナドのどこがわかりにくいのだろうか
- 手続き型(オブジェクト指向)では、状態をたくさん設定し、状況に応じて個別に更新していく
- → runStateで扱うStateは一つで、それを更新していく。たくさんの状態のように扱うには、それらすべてをレコードメンバーなどにして持った大状態を定義して使うことになる(ゲームのセーブデータ全体の定義みたいな感覚)。
- State App関数で状態更新手続きと状態を利用した処理を一緒に書かけるが、あえて混ぜたもの中心に説明がなされる
- → 更新と計算は分けたほうがわかりやすそう
- 他との組み合わせ方法が主題として書かれていない
- IOとでもStateの解説のところには例にない