モナドにログ機能を付与するWriterT

 場合によっては,StateモナドやWriterモナド以外のモナドを使っている部分でログを作成したくなることもあります。Writerモナドをより広い範囲で活用するには,Readerはただのモナドであるよりもモナド変換子であったほうが便利です。StateモナドやReaderモナドと同様に,Writerモナドにもモナド変換子版のWriterTが存在します。WriterTモナド変換子の定義は以下の通りです。

newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }

execWriterT :: Monad m => WriterT w m a -> m w
execWriterT m = do
    ~(_, w) <- runWriterT m
    return w

~ 略 ~

instance (Monad m) => Functor (WriterT w m) where
    fmap f m = WriterT $ do
        ~(a, w) <- runWriterT m
        return (f a, w)

instance (Monoid w, Monad m) => Monad (WriterT w m) where
    return a = WriterT $ return (a, mempty)
    m >>= k  = WriterT $ do
        ~(a, w)  <- runWriterT m
        ~(b, w') <- runWriterT (k a)
        return (b, w `mappend` w')
    fail msg = WriterT $ fail msg

 他のモナド変換子と同じように,runWriterTを使って取り出したモナドmでの計算としてWriterモナドを再定義することで,Writerモナドとモナドmとの合成を実現しています。

 MonadWriterクラスやMonadTransクラスも同様の形で定義されています。

instance (Monoid w, Monad m) => MonadWriter w (WriterT w m) where
    tell   w = WriterT $ return ((), w)
    listen m = WriterT $ do
        ~(a, w) <- runWriterT m
        return ((a, w), w)
    pass   m = WriterT $ do
        ~((a, f), w) <- runWriterT m
        return (a, f w)

-- ---------------------------------------------------------------------------
-- Instances for other mtl transformers

instance (Monoid w) => MonadTrans (WriterT w) where
    lift m = WriterT $ do
        a <- m
        return (a, mempty)

instance (Monoid w, MonadIO m) => MonadIO (WriterT w m) where
    liftIO = lift . liftIO

 これまで説明したことの繰り返しなので,意味はわかると思います。注目すべきなのは,liftメソッドがmemptyを使って空のログを作成することくらいでしょう。

 では実際に使ってみましょう。WriterTモナド変換子をIOモナドに適用したWriterT IOモナドを例に挙げます。

 ReaderTモナド変換子のところで説明したように,GHCiのプロンプト上ではモナドmをIOモナドだと判断して実行します。runWriterをrunWriterT,execWriterをexecWriterTにそれぞれ変更すれば,WriterT IOモナドでの実行結果が得られます。

Prelude Control.Monad.Writer> runWriterT $ tell "category"
((),"category")
Prelude Control.Monad.Writer> execWriterT $ tell "category"
"category"
Prelude Control.Monad.Writer> execWriterT $ tell "semi-" >> tell "group"
"semi-group"
Prelude Control.Monad.Writer> execWriterT $ return () >> tell "category"
"category"
Prelude Control.Monad.Writer> execWriterT $ tell "category" >> return ()
"category"
Prelude Control.Monad.Writer> runWriterT $ listen $ tell "group"
(((),"group"),"group")
Prelude Control.Monad.Writer> runWriterT $ tell "semi-" >> listen (tell "group")
(((),"group"),"semi-group")
Prelude Control.Monad.Writer> runWriterT $ tell "group" >> listen (return ())
(((),""),"group")
Prelude Control.Monad.Writer> runWriterT $ listens ("semi-" ++) $ tell "group"
(((),"semi-group"),"group")
Prelude Control.Monad.Writer> runWriterT $ censor ("semi-" ++) $ return "group"
("group","semi-")
Prelude Control.Monad.Writer> runWriterT $ censor ("semi-" ++) $ tell "group"
((),"semi-group")

 Writerモナドを使った場合と同じ結果になっているのがわかりますね。

 リスト以外の要素をログの書き込みに使った場合でも,Writerモナドと同じ結果になります。

Prelude Control.Monad.Writer> execWriterT $ tell (All True) >> tell (All False)
All {getAll = False}
Prelude Control.Monad.Writer> execWriterT $ tell (All False) >> tell (All True)
All {getAll = False}
Prelude Control.Monad.Writer> execWriterT $ tell (All True) >> tell (All True)
All {getAll = True}
Prelude Control.Monad.Writer> execWriterT $ tell (Any True) >> tell (Any False)
Any {getAny = True}
Prelude Control.Monad.Writer> execWriterT $ tell (Any False) >> tell (Any True)
Any {getAny = True}
Prelude Control.Monad.Writer> execWriterT $ tell (Any True) >> tell (Any True)
Any {getAny = True}
Prelude Control.Monad.Writer> execWriterT $ tell (Any False) >> tell (Any False)
Any {getAny = False}

 今度はliftIOを使い,WriterTモナド変換子が保持するログを表示してみましょう。

module WriterT where
import Control.Monad.Writer

printLog m = do
    ~(_, w) <- listen m
    liftIO $ print w

printLog' m = do
    ~(_, w) <- listens (liftIO . print) m
    w

*WriterT> runWriterT $ printLog $ tell (Any False)
Any {getAny = False}
((),Any {getAny = False})
*WriterT> runWriterT $ printLog' $ tell (Any False) >> tell (Any True)
Any {getAny = True}
((),Any {getAny = True})
*WriterT> execWriterT $ printLog $ tell (Any False)
Any {getAny = False}
Any {getAny = False}
*WriterT> execWriterT $ (printLog $ tell (Any False)) >> tell (Any True)
Any {getAny = False}
Any {getAny = True}
*WriterT> execWriterT $ (printLog $ tell (Any True)) >> tell (Any False)
Any {getAny = True}
Any {getAny = True}
*WriterT> execWriterT $ (printLog $ tell "semi" >> tell "-" ) >> tell "group"
"semi-"
"semi-group"
*WriterT> execWriterT $ (printLog' $ tell "semi" >> tell "-" ) >> tell "group"
"semi-"
"semi-group"

 計算の最終結果を表示する前に,printLogまたはprintLog'関数に渡した式での中間状態のログが表示されているのがわかると思います。

著者紹介 shelarcy

 unsafePeformIOやグローバル変数の安易な使用は,Haskell初心者が陥りがちな罠の一つです。最近でも,2009年1月から2月にかけて,これらの使用に関する論争が行われていました(参考リンク1参考リンク2参考リンク3)。

 こうしたものは,Haskellで用意されている別の機能で置き換えられます。今回は,グローバル変数やunsafePerformIOに代わる手段として二つのモナドを説明しました。

 これらのモナドは,今回説明した以上の能力を持っています。しかし,様々なモナドについて紹介している「All About Monads」(日本語訳:モナドのすべて)という記事にはあっさりとした説明しかなく,これらのモナドを使うことで「何ができて,どううれしいのか」を伝えきれていないように感じました。今回の記事でそうしたことが伝われば幸いです。