Stateモナド
今まですごいH本を何回読んでも理解できなくて、理解できない度に諦めて、間を置いたらまた解んなくなっての悪循環を繰り返してきたStateモナド。それが今日やっと理解できた気になったので忘れないうちに思い出を綴ります。
Stateモナドは定義にしてたったの数行
instance Monad (State s) where
return x = state $ \s -> (x,s)
(State h) >>= f = state $ \s -> let (a, newState) = h s
(State g) = f a
in g newState
returnはわかります。現在の値を文脈に入れるだけだから。
>>=がこうなってる理由が全然わからなかった。
ところでMaybe、List
話は変わってState以外のこれらのモナドについて。これらは包むものが具体的です。
実数データだったり代数データだったりするので、現実世界と関連付けやすく具体的なイメージが湧きやすいです。Maybeは中身があるかどうかわからない箱、リストは候補がいっぱい詰まった箱。どれもわかりやすい。
けど、Stateって箱には何が詰まってるの?
マリオ
さらに話は変わって、今日ふとマリオを見た時に「マリオにも状態があるんだよなぁ」となんとなく思いました。そしてキノコを食ったらチビからデカに変わるって考えた時に「もしやこれがStateなのか?」と直感しました。
というわけでマリオの変化をコードに落としてみました。
import Control.Monad.State
data Mario = Chibi | Deka | Die deriving (Show, Eq)
type Shout = String
kinoko :: State Mario Shout
kinoko = state kinoko'
where kinoko' s
| s == Die = ("...", Die)
| otherwise = ("hyahoo!!", Deka)
kuribo :: State Mario Shout
kuribo = state kuribo'
where kuribo' s
| s == Deka = ("mamma-mia", Chibi)
| otherwise = ("...", Die)
マリオは状態を持つデータです。チビ時々デカです(よく臨終されてもいます)。そしてキノコがStateモナドです(ついでにクリボーも)。
キノコはチビマリオという状態を受け取り奇声を発しながらマリオの状態をデカマリオ変化させます。そしてくりぼーに当たるとマリオは奇声とともにデカマリオからチビマリオ、チビマリオは臨終に変化させます。
試してみましょう。
*Main> runState kinoko Chibi
("hyahoo!!",Deka)
*Main> runState kuribo Deka
("mamma-mia",Chibi)
*Main> runState kuribo Chibi
("...",Die)
うまく動いています。
Stateは状態じゃなくて状態遷移
今まではStateって言うくらいだから、Stateの箱に入ってるものは状態そのもの(つまりマリオ)が入ってるんだっていう前提でコードを理解しようとしてました。理解の方向が全く違ってたので理解ができなかったんだなと。
箱にキノコを入れてみて理解が一気に進みました。
>>=
箱の中身がわかったところでバインドはどう理解しましょう。
まずはh s
で現在の状態を取り出します。んでもって次に取り出した値を関数で移します。写した結果、新しい状態遷移が取得できる云々。
こんなことはコードを読めば分かる事なので、具体的な事はキノコを移す関数を作って観察します。
tamago :: Shout -> State Mario Shout
tamago x = state $ \s -> (take 3 x ++ "...Detteiu!!", Die)
このタマゴをかけたキノコを食べると、「でっていう」という謎の言葉を残して絶命してしまいます。
試してみましょう。
*Main> runState (kinoko >>= tamago) Chibi
("hya...Detteiu!!",Die)
*Main> runState (kinoko >>= tamago) Deka
("hya...Detteiu!!",Die)
デカマリオさえも1撃でしとめる卵。サルモネラ菌ってほんと恐ろしいですね。
けど過程がよくわからんかったので、少しずつ展開していきます。
--まずは h を kinoko に展開
let (a, newState) = kinoko s
(State g) = f a
in g newState
--sがChibiに適用される予定でkinokoを適用
--aが"hyahoo", newStateがDekaとなる
let ("hyahoo", Deka)
(State g) = f "hyahoo"
in g Deka
--"hyahoo"にf (=tamago)を適用
let ("hyahoo", Deka)
\s -> ("hya...Detteiu!!", Die)
in g newState
--最後にDeka(元newState)をg(元tamago)に適用
("hya...Detteiu!!", Die)
状態を遷移させたあとのデータにそれぞれ関数を適用させています。
こうやって見れば他のモナドとやってる事は変わんないのですね。
さいごに
足掛け数年、キノコという具体的なものを想像してやっとStateが理解出来た気になりました。まさか子供の頃に散々見てたマリオが数十年後にこんな形で役に立つとは思いもしませんでした。