MonadStateクラス
Stateモナドについて理解したところで,いよいよ「状態」の取得を行うget,「状態」の書き換えを行うputという二つの関数について見ていきます。これら二つの関数は,Stateという型単体に対して定義されているわけではありません。今回は特に説明しませんが「モナド変換子(Monad Transformer)」であるStateTに対しても定義できるよう,MonadStateという型クラスのメソッドとして宣言されています。
class (Monad m) => MonadState s m | m -> s where get :: m s put :: s -> m () instance MonadState s (State s) where get = State $ \s -> (s, s) put s = State $ \_ -> ((), s)
MonadStateにはこれまで見てきた型クラスとは少し違うところがあります。まず,型変数を二つ持っています。これは次期標準Haskell'で追加される予定の拡張機能,「多引数型クラス(MPTC: Multi-Parameter Type Class,複数パラメータ型クラス)」によって実現できます。型変数を複数使用できるということで便利な多引数型クラスですが,この機能には一つ欠点があります。型変数が一つではなく複数あることにより,型推論が失敗してしまう可能性があるのです。型推論が失敗したときにエラーが生じる場合はいいですが,悪くすると正しく型検査(type checking,型チェック)を行えないという結果をもたらしてしまいます。この問題を解決するには,型推論のやり方について何らかの注釈を与えてやる必要があります。
そのための機能の一つが,|で区切って右側に記述されている「関数従属性(FD: Functional Dependencies)」です。関数従属性にはいろいろと難しい部分があり,機能について解説するだけで一つの記事になってしまうので,ここでは詳しくは説明しません。今は,->の左側にある型によって右側の型が一意に決定される,ということがわかれば十分です。
Stateの場合には,型変数mであるモナドがState sに決まった時点で,s (State s)というインスタンスに結び付けられます。この結果,sとState s'のs'が同じ型に推論されることになります。
メソッドの定義は,見たままです。getは「状態」を新しい値として取得します。putはこれまでの状態を捨て,新しい「状態」に置き換えます。この「状態」の更新という操作が行われたことを示すために,putは()を値として返しています。
Prelude Control.Monad.State> runState (return 12 >> get) 11 (11,11) Prelude Control.Monad.State> runState (return 12 >> put 10) 11 ((),10) Prelude Control.Monad.State> runState (return 12 >> put 10 >> return 12) 11 (12,10)
この例では単にgetで「状態」を取得したりputで「状態」を更新したりしているだけですが,多くの場合,単に「状態」を取得するのではなく,取得した「状態」に対して何らかの操作を行うのが普通だと思います。
Prelude Control.Monad.State> runState (return 12 >> get >>= \s -> return $ s+1) 11 (↑実際には1行) (12,11) Prelude Control.Monad.State> runState (return 12 >> get >>= \s -> (put $ s+1)) 11 (↑実際には1行) ((),12) Prelude Control.Monad.State> runState (do {return 12; s <- get; put $ s+1; return s}) 11 (↑実際には1行) (11,12)
そのため,利便性のためにgetとputのほかに以下の高階関数が定義されています。
gets :: (MonadState s m) => (s -> a) -> m a gets f = do s <- get return (f s) modify :: (MonadState s m) => (s -> s) -> m () modify f = do s <- get put (f s)
それぞれ,getsは「状態」から取得した「値」に関数を適用すること,modifyは関数を適用して「状態」を変更することを目的とした関数です。
Prelude Control.Monad.State> runState (return 12 >> gets (+1)) 11 (12,11) Prelude Control.Monad.State> runState (return 12 >> modify (+1)) 11 ((),12) Prelude Control.Monad.State> runState (modify (+1) >> return 12) 11 (12,12) Prelude Control.Monad.State> runState (get >>= \s -> modify (+1) >> return s) 11 (↑実際には1行) (11,12)
今回のまとめ
今回は局所的な「状態」を扱うための糖衣構文であるStateモナドについて説明しました。第3回を読んでいたときにはおぼろげだった話の輪郭が,だんだんと見えるようになってきたのではないでしょうか?
Stateモナドには,他にもいくつかの親戚があります。Stateモナドの役割を制限し,主に「状態」の読み取りを行うために使われるReaderモナドや,ログの出力など主に新たな「状態」を書き込むために使われるWriterモナドなどです。スペースの都合上,今回はこれらについて解説しませんでしたが,これらもStateモナドと同じくmtlに収録されているので,興味のある人は試してみるとよいでしょう。
また,最初に述べた参照型を試してみるのもよいかもしれません。IO版がData.IORefモジュールにあります。Data.IORefの関数は名前通りの関数ばかりなので,(一部の関数を除けば)すぐに使い方を理解することができると思います(他にST(State Transformer)モナドで使うことのできるST版というものがあるのですが,いくつか特徴的な部分があるのでドキュメントを見ただけで使用するのは難しいでしょう。これも場を改めて説明したいと思います)。
mapStateとwithState Control.Monad.Stateモジュールには,他にmapStateやwithStateという関数があります。 mapStateは,Stateに対するもう一つのmapです。fmapとの違いはa -> bという型を持つ関数ではなく,(a, s) -> (b, s)という型を持つ関数を対象に定義されているところにあります。 mapState :: ((a, s) -> (b, s)) -> State s a -> State s b mapState f m = State $ f . runState m このため,mapStateでは「値」だけではなく,「状態」のほうに対しても変化を加えることができます。 Prelude Control.Monad.State> :t mapState (\(x,y) -> (x+1, y+1)) (return 12) (↑実際には1行) mapState (\(x,y) -> (x+1, y+1)) (return 12) :: (Num s, Num a) => State s a (↑実際には1行) Prelude Control.Monad.State> runState (mapState (\(x,y) -> (x+1, y+1)) (return 12)) 11 (↑実際には1行) (13,12) もちろん,ペアのうち第1要素にだけ影響を与える関数を使えば,fmapと同じ機能を持つ関数として使用できます。Control.Arrowモジュールにはa -> bという関数を「ペアのうち片方の要素にだけ関数を適用する関数」に変換する関数が定義されているので,これを使って試してみることにしましょう。firstが「a -> bという関数を,ペアのうち第1要素にだけ影響を与える関数に変換する」という関数です。 Prelude Control.Monad.State Control.Arrow> runState (mapState (first (+1)) (return 12)) 11 (↑実際には1行) (13,11) Prelude Control.Monad.State Control.Arrow> runState (fmap (+1) (return 12)) 11 (↑実際には1行) (13,11) fmapとは逆に「状態」に対してのみ関数を適用したいという場合には,withStateが使えます。 withState :: (s -> s) -> State s a -> State s a withState f m = State $ runState m . f これもやはりmapStateとControl.Arrowのsecondを使うことで,すぐに同様の機能を再現することができます。 Prelude Control.Monad.State Control.Arrow> runState (withState (+1) (return 12)) 11 (↑実際には1行) (12,12) Prelude Control.Monad.State Control.Arrow> runState (mapState (second (+1)) (return 12)) 11 (↑実際には1行) (12,12) |
著者紹介 shelarcy 関数従属性は,すでに長年HugsとGHCの両処理系で拡張機能として使われてきたことから,Haskell'に入るはずだと思い,今回の原稿に取り掛かる前に下調べしていました。しかしHaskell'のページで確認したところ,以下の理由により実際にはまだどうするか決まっていないようです(参考リンク)。
というわけで,まだそんなに詳しい説明が必要ないことも考えると,少々先走ってしまったかもしれません。CHRは,GHCの開発版に最近取り入れられた「GADTと型クラスの間で生じていた問題を解決し,正しく型検査できるようにする」ための仕組みの理論的背景にもなっているため,このあたり深追いしていくと,いろいろと面白くはあるのですが…。 |