PR

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'のページで確認したところ,以下の理由により実際にはまだどうするか決まっていないようです(参考リンク)。

  1. 関数従属性の細部は非常に難しいこと
    1. (特に他の処理系拡張機能との共存を考えると)正しく動くように実装するのが難しい
    2. (C++でtemplateを使ったコードにおいて生じるような類の)長く分かり難い型エラーが生じる
    3. 一応,Prologなどの論理プログラミングの領域で使われているCHR(Constraint Handling Rules)を使って関数従属性を基礎付けする,という解決策が考えられている。しかし,まだ完全な解決策にはなっていない(参考リンク1参考リンク2参考リンク3)。
  2. 最近になって,C++の特性クラス(Traits Class)で使われている「関連型(AT:Associated Type)」という概念(参考リンク)を導入し,関数従属性の代わりに使用しようという動きがあること
    1. 実際,GHCの開発版(GHC HEAD)には,関連型のサポートを含んだ拡張の成果が既にマージされている
    2. しかし,実際のプログラミングにおける使用経験が不足しているため,本当に関数従属性の代わりになりえるかどうか証明されていない

 というわけで,まだそんなに詳しい説明が必要ないことも考えると,少々先走ってしまったかもしれません。CHRは,GHCの開発版に最近取り入れられた「GADTと型クラスの間で生じていた問題を解決し,正しく型検査できるようにする」ための仕組みの理論的背景にもなっているため,このあたり深追いしていくと,いろいろと面白くはあるのですが…。