7
6

More than 5 years have passed since last update.

函数モナドからStateモナドに進む

Last updated at Posted at 2013-12-30

ReaderからStateへ

函数モナド a.k.a. Readerモナドをある程度理解したので、Stateモナドまで足を伸ばしてみようというわけである。文系情弱にStateモナドが理解できるのだろうか。

Stateは「状態」を表現する型sと「値」を格納する型aを取る。函数モナドで見たように、これはState s型のモナドで、sが共通のモナド同士でやりとりをすることになる。型図式のmがState sになるわけね。

超基礎的なところから考えなおしてみる。まず、Haskellで「状態」を実現する一番簡単なやり方は、引数を増やして状態を渡していくことだ。一番初歩的な例は階乗函数の定義に蓄積引数を使うやり方である。またIOモナドが実は隠れた引数として「世界の状態」を取っていると考えられるのと同様にして、状態に応じて挙動を変える函数とは、「状態」を表現するような引数を持つ函数にほかならない。

Stateモナドの中身は、そのデータ構築子State(ただし非公開なので代わりにstateを使う)がs->(a,s)型の函数を取ってState s a型の値を返してくることからわかるように、「状態」を取って「計算結果」と「状態のタプル」を返してくる函数である。このState型に格納された函数が、「状態に応じて挙動が変わる函数」であって、その中身は、渡された状態に応じてある値を返し(これが「計算結果」で)、その値の計算に付随して内部状態がどう変化するかをも返す函数である。つまり状態値stを取って、その状態値に応じた値valを返し、更に適宜更新された状態値st’を返す函数である。State s a型の実体は s->(a,s)型の函数にnewtypeしたものに過ぎず、この実体を取り出すにはrunState函数を使えばよい(runState (State型の値) (状態値)という形でイディオム的に使われるのでStateモナドに初期状態を与えて駆動(run)する函数であるかのように見えるところがポイントだが、その実は単にStateモナドの中身の函数を取り出しているだけである)。Stateデータ構築子によるパターンマッチは使えないことになっているが、以下では便宜的にそれを用いて表記する(必要ならrunStateを使って取り出す形で書けるので問題ないだろう)。

呼ばれるたびに内部に保持したカウンターを参照してカウント・ダウンする(という挙動を表現する)函数は

countdown :: Int -> (String, Int)
countdown 0 = (BANG!!, undefined)
countdown n = (show n, n-1)

と書ける(ゼロまでカウントするとundefinedを評価して例外が起きる)。Stateモナドのインスタンスであるクラスの値は、したがって、「状態」を表現する引数を取る函数によって、内部状態を持つ手続型的「関数」を表現するものであることになる。returnは受け取った値をState s モナドの値にして返すが、State s モナドへの変換にあたってミニマルな操作しかしないので、sには触ってはいけない。つまり:

return :: a -> State s a
return a = \st -> (a, st)

という、どんな状態を受け取ろうがお構いなくreturnでモナドに突っ込んだ値aを返してくる函数であることになる(受け取った状態はタプルの第二成分に格納しているが、やっていることは実質的に函数モナドの時と変わらない)。モナドなので最低限、後は(>>=)が理解できればいい:

(State x) >>= f = State $ \st -> 
                          let (val,st) = x st 
                           in runState (f val) st

xは上のcountdownのようなs->(a,s)型の函数である。(>>=)の結果はある函数を返してくる。それは、ある状態値stを取って、それをxに投入して得た計算結果valと更新された状態st’を使い、(>>=)の連鎖先の函数fvalを投入して得られた(State s b型の値の中身である)s->(b, s)型にst’を投入して得られた(計算結果, 更なる更新状態)のタプルを返してくるような函数である。これは直観的にわかりやすい。xfを連鎖させようと思えば、あるs型の値stxに投入して得られた結果と更新された状態を共にfに投入すればよい。fに結果valを投入するとs->(b,s)型函数が返ってくるのだから、それに当該の更新された状態を続けて投入してやるだけのことである。こうして、この連鎖の全体はs型の値を投入されて(b,s)型の値を返してくるState s b型の値である。(>>=)で連鎖させた計算の全体はStateモナド値なので、runStateで中身の函数を取り出して初期状態を食わせれば、連鎖された一連の計算が実行され、それらの計算の最終結果と最終状態のタプルが返ってくるわけだ。わかりやすい。

fmap f (State x) = State $ \st -> 
                   let (val, st) = x st
                    in (f val, st)

fmapは同様に(そして更に)明瞭である。Stateモナドの値が返ってくるわけだが、それは、ある状態値stを取って、それをxに投入して得た計算結果valfを作用させた結果と更新された状態st’を返してくる函数である。つまりfvalの部分にのみ作用するだけである。

有名な『モナドのすべて』から写経すると:

getAny :: (Random a) => State StdGen a
getAny = do g      <- get
            (x,g') <- return $ random g
            put g'
            return x

StdGen型の値gは乱数生成器で、random函数はStdGen型の値を取って(a, StdGen)型の値を返してくる。 見事にStateモナドと平仄が合わせてあるわけだ。State s a型の値の実体は、s型の値を引数とする函数で、runState函数でその実体・中身が取り出され、そこに更に引数としてs型の値を食わせると、連鎖的に計算が進んでいく。この、後で引数として渡されることになるs型の値は、Stateモナドによる計算連鎖中にgetputでアクセスできる。getで、この値を得ることができる(計算実行時にgetAnyに渡されることになる乱数生成器が返ってくる)。putで、計算連鎖中でこの計算の後にくる計算に渡される新状態(ここではrandom函数が乱数値xとともに生成してくれた新たな乱数生成器g’)を設定する。この計算の返り値自体はもちろん当の乱数値xで、それはreturn xでStateモナドにくるまれて返される(その実体は自分に渡された状態値gを受け取って、先にputで設定した更新状態値g’と計算結果xのタプル(x, g’)を返す函数にほかならない)。

うん、理屈はわかりやすい。(>>=)の定義なども全般にReaderモナドよりも素直でわかりやすいと思う。Readerモナドは函数型構築子(->)がモナドであるところ、それに無理やり「計算連鎖に不変的な環境状態を暗黙に供給し続ける」というReaderモナドとしての意味を読み込んだようなところがあるもんな。

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6