あらかじめ用意されたRWSモナド

 これまで説明したように,Reader,Writer,Stateの三つのモナドを合成する方法では,ソースコードがわかりにくくなります。それに加え,合成モナドの演算は合成元のモナドに委譲されていくため,モナド変換子を使ってモナドの階層をいくつも組み合わせると,処理の効率が落ちるという問題もあります。

 そこで,Reader,Writer,Stateをプログラマが合成する代わりに,それぞれの能力を併せ持つモナドがあらかじめ用意されています。mtlパッケージのControl.Monad.RWS配下の二つのモジュールで提供されているRWSモナドです。RWSの元になったWriterモナドとStateモナドには正格モナド版と遅延モナド版が存在するため,RWSモナドにも正格モナド版と遅延モナド版の2種類があります。正格モナド版はControl.Monad.RWS.Strict,遅延モナド版はControl.Monad.RWS.Lazyでそれぞれ定義されています。WriterモナドやStateモナドと同様に,Control.Monad.RWSモジュールではControl.Monad.RWS.Lazyモジュールの構成要素をそのまま再エクスポートするように定義されています。そのため,以降は遅延モナド版を使って説明します。

 RWS型はReader型の「r -> a」,State型の「a -> (s, a)」,Writer型の「(a, w)」を組み合わせた型として定義されています。

newtype RWS r w s a = RWS { runRWS :: r -> s -> (a, s, w) }

newtype Reader r a = Reader {
~ 略 ~
    runReader :: r -> a
}

newtype State s a = State { runState :: s -> (a, s) }

newtype Writer w a = Writer { runWriter :: (a, w) }

 つまり,RWSモナドは,共有する環境rと状態を取り,「値,状態,ログの三つを組にして返す関数」を内部に持ちます。

 RWSモナドのFunctorクラスとMonadクラスのインスタンスの定義は以下の通りです。

instance Functor (RWS r w s) where
    fmap f m = RWS $ \r s -> let
        (a, s', w) = runRWS m r s
        in (f a, s', w)

instance (Monoid w) => Monad (RWS r w s) where
    return a = RWS $ \_ s -> (a, s, mempty)
    m >>= k  = RWS $ \r s -> let
        (a, s',  w)  = runRWS m r s
        (b, s'', w') = runRWS (k a) r s'
        in (b, s'', w `mappend` w')

 これらのインスタンスもReader,State,Writerの組み合わせとして定義されています。

instance Functor (Reader r) where
    fmap f m = Reader $ \r -> f (runReader m r)

instance Monad (Reader r) where
    return a = Reader $ \_ -> a
    m >>= k  = Reader $ \r -> runReader (k (runReader m r)) r

instance Functor (State s) where
    fmap f m = State $ \s -> let
        (a, s') = runState m s
        in (f a, s')

instance Monad (State s) where
    return a = State $ \s -> (a, s)
    m >>= k  = State $ \s -> let
        (a, s') = runState m s
        in runState (k a) s'

instance Functor (Writer w) where
    fmap f m = Writer $ let (a, w) = runWriter m in (f a, w)

instance (Monoid w) => Monad (Writer w) where
    return a = Writer (a, mempty)
    m >>= k  = Writer $ let
        (a, w)  = runWriter m
        (b, w') = runWriter (k a)
        in (b, w `mappend` w')

 MonadReader,MonadWriter,MonadStateクラスのインスタンスは,RWSの型に合わせる形で再定義されています。元になったモナドのメソッドとほぼ同様の定義になっています。異なるのは,ログを操作しないメソッドに対してmemptyを使う点と,値の参照を行う必要のない場所でワイルドカード・パターンを使う点だけです。

instance (Monoid w) => MonadReader r (RWS r w s) where
    ask       = RWS $ \r s -> (r, s, mempty)
    local f m = RWS $ \r s -> runRWS m (f r) s

instance (Monoid w) => MonadWriter w (RWS r w s) where
    tell   w = RWS $ \_ s -> ((), s, w)
    listen m = RWS $ \r s -> let
        (a, s', w) = runRWS m r s
        in ((a, w), s', w)
    pass   m = RWS $ \r s -> let
        ((a, f), s', w) = runRWS m r s
        in (a, s', f w)

instance (Monoid w) => MonadState s (RWS r w s) where
    get   = RWS $ \_ s -> (s, s, mempty)
    put s = RWS $ \_ _ -> ((), s, mempty)

 ただし,RWSモナドには型検査の面で問題が一つあります。MonadReader,MonadWriter,MonadStateの三つのクラスに対するインスタンス宣言を見ればわかるように,RWS型はそれぞれの関数従属性「m -> b」のmの部分である「RWS r w s」で,bの部分に来るr,w,sのすべての型変数を持っています。これにより対応範囲条件が満たされ,モナド変換子を使って合成する際に起こる可能性がある「対応範囲条件を撤廃することに伴う問題」を避けられます。

 しかしRWS型は多くの型変数を持つため,それぞれのインスタンス宣言で「MonadReader,MonadWriter,MonadStateの各クラス単体の関数従属性による制約では固定されない型変数」が関数従属性「m -> b」の型変数mに当たる「RWS r w s」に出てきます。例えば,MonadWriterクラスのインスタンスでは,bはwに相当するため,残りの型変数であるrとsを固定できせん。RWSモナドでは,こうした問題の発生を防ぐため,それぞれのクラスを継承したMonadRWSクラスを別途用意し,すべての型変数を確定させるために新たな関数従属性による制約を定義しています。

class (Monoid w, MonadReader r m, MonadWriter w m, MonadState s m)
   => MonadRWS r w s m | m -> r, m -> w, m -> s

instance (Monoid w) => MonadRWS r w s (RWS r w s)

 ReaderモナドのwithReader関数に対応する形で,RWSモナド用のlocalメソッドを一般化するための関数も用意されています。ただし,localメソッドを一般化するだけでなく,withState関数の機能を取り込んだ「共有する環境と状態を変更する機能」を持つwithRWS関数として定義されています。この関数の定義は以下の通りです。

withRWS :: (r' -> s -> (r, s)) -> RWS r w s a -> RWS r' w s a
withRWS f m = RWS $ \r s -> uncurry (runRWS m) (f r s)

 uncurryはPreludeで用意されている関数で,「a -> b -> c型」の2引数の関数を「(a, b) -> c型」の対を取る1引数の関数に変換します(参考リンク)。

Prelude> :t uncurry
uncurry :: (a -> b -> c) -> (a, b) -> c

 withRWSはwithReaderとwithStateの機能を併せ持つため,関数fは「r' -> s -> (r, s)」という型を持つよう定義されています。一方,RWS型が保持できる関数の型は「r -> s -> (a, s, w)」です。このままでは,RWSモナドが作成する関数と関数fを合成できません。そこでcurryを使って,RWS型に保持されている「r -> s -> (a, s, w)」を「(r, s) -> (a, s, w)」に変換することで関数合成を実現しているのです。

 RWSモナドでは,StateモナドのevalStateとexecState,WriterモナドのexecWriterに対応する関数も用意されています。ただし,RWSモナドの関数は,ログと対にした値を必ず返すようになっています。

evalRWS :: RWS r w s a -> r -> s -> (a, w)
evalRWS m r s = let
    (a, _, w) = runRWS m r s
    in (a, w)

execRWS :: RWS r w s a -> r -> s -> (s, w)
execRWS m r s = let
    (_, s', w) = runRWS m r s
    in (s', w)

 実際に使ってみましょう。

Prelude Control.Monad.RWS> runRWS (ask >>= tell) "group" 0
((),0,"group")
Prelude Control.Monad.RWS> runRWS (local ("semi-" ++) ask >>= tell) "group" 0
((),0,"semi-group")
Prelude Control.Monad.RWS> runRWS (withRWS (\_ s -> ("category", s)) ask >>= tell) () ()
((),(),"category")
Prelude Control.Monad.RWS> runRWS (listen $ ask >>= tell) "group" 0
(((),"group"),0,"group")
Prelude Control.Monad.RWS> runRWS (listens ("semi-" ++) $ ask >>= tell) "group" 0
(((),"semi-group"),0,"group")
Prelude Control.Monad.RWS> runRWS (censor ("semi-" ++) $ ask >>= tell) "group" 0
((),0,"semi-group")
Prelude Control.Monad.RWS> runRWS (get >>= tell) "group" (Any False)
((),Any {getAny = False},Any {getAny = False})
Prelude Control.Monad.RWS> runRWS (get >>= tell >> tell (Any True)) "group" (Any False)
((),Any {getAny = False},Any {getAny = True})
Prelude Control.Monad.RWS> runRWS (get >>= tell >> put (Any True)) "group" (Any False)
((),Any {getAny = True},Any {getAny = False})
Prelude Control.Monad.RWS> runRWS (ask >>= tell >> return 0) "group" "category"
(0,"category","group")
Prelude Control.Monad.RWS> evalRWS (ask >>= tell >> return 0) "group" "category"
(0,"group")
Prelude Control.Monad.RWS> execRWS (ask >>= tell >> return 0) "group" "category"
("category","group")

 Readerモナド,Writerモナド,Stateモナドからモナドを合成した場合と同様に,三つのモナドのメソッドや関数を組み合わせて使用できています。

 RWSモナドを使う際に注意すべき点は,RWSモナドはReaderモナドやWriteモナド,Stateモナドを置き換えるものではないということです。前回説明したように,Readerモナドは「読み取り専用」,Writerモナドは「書き込み専用」としてStateモナドの機能を制限したものです。RWSモナドを使うと,そうした意図がソースコードから読みとれなくなってしまいます。

 また,RWSモナドはReaderモナドやWriteモナド,Stateモナドの全ての引数や返り値を保持するため,これらのモナドの代わりに使うと,runRWSを使って取り出される関数に不必要な引数や返り値が出てきます。こうした余計な引数や返り値は,ソースコードを不明瞭にします。

 ReaderモナドやWriterモナド,Stateモナドを単体で使うときには,RWSモナドではなくそうしたモナドを使うべきです。これらのモナドの機能を組み合わせて利用する必要があるとき,はじめてRWSモナドを利用すべきでしょう。

 必要であれば,「ReaderモナドとWriterモナドの機能だけを持ったRWモナド」といったRWSモナドの機能の一部を持つモナドを自分で定義して利用することもできます。これまで説明したReaderモナドやWriterモナド,Stateモナド,RWSモナドの定義を参考にすれば,そうしたモナドを定義するのは難しくないはずです。