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の解説のところには例にない