より使いやすいReaderTモナド変換子

 StateモナドやReaderモナド以外のモナドを使っている部分で,値を参照するコードを利用したい場合もあるでしょう。より広い範囲でReaderモナドを活用するには,ただのモナドであるよりもモナド変換子であったほうが便利です。Stateモナドと同様に,Readerモナドにもモナド変換子版であるReaderTが存在します。ReaderTモナド変換子の定義は以下の通りです。

newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }

~ 略 ~

instance (Monad m) => Functor (ReaderT r m) where
    fmap f m = ReaderT $ \r -> do
        a <- runReaderT m r
        return (f a)

instance (Monad m) => Monad (ReaderT r m) where
    return a = ReaderT $ \_ -> return a
    m >>= k  = ReaderT $ \r -> do
        a <- runReaderT m r
        runReaderT (k a) r
    fail msg = ReaderT $ \_ -> fail msg

 runReaderTを使って取り出したモナドmでの計算としてReaderモナドを再定義することで,Readerモナドとモナドmとの合成を実現しています。

 ReaderT型のMonadReaderクラスのaskとlocalの定義は,Reader型に対するインスタンスでの定義とは異なっているように見えます。しかし,少し考えれば等価な定義であることがわかります。

instance (Monad m) => MonadReader r (ReaderT r m) where
    ask       = ReaderT return
    local f m = ReaderT $ \r -> runReaderT m (f r)

 askメソッドの定義である「ReaderT return」でのreturnは,Readerモナドが保持する値であるrをモナドm上で返すよう働きます。Readerモナドでのaskの定義はidなので,単純に考えればReaderTモナド変換子でのaskの定義はidとreturnを合成した式を使った「ReaderT $ return . id」となるでしょう。ですが,idは値をそのまま返す恒等関数なので,idとの関数合成は式から省略できます。結果として,「ReaderT return」という定義になっているのです。

 localメソッドは,ラムダ抽象を使って明示的に変数rを取得しています。変数rを取らないように関数合成の形で書き直すと,この式はReaderモナドでのlocalの定義と同じ「ReaderT $ runReaderT m . f」になります。実際に,withReaderT関数はこの形で定義されています。

withReaderT :: (r' -> r) -> ReaderT r m a -> ReaderT r' m a
withReaderT f m = ReaderT $ runReaderT m . f

 モナドを合成する以上,ReaderTモナド変換子を使って合成したReaderT mからは,Readerモナドの機能だけでなく,もう一つの合成元であるモナドmの機能も利用できる必要があります。これは第15回で説明したように,MonadTransクラスのliftメソッドやMonadIOクラスのliftIOメソッドなどを使い,モナドmに対する演算を合成後のモナドReaderT mに対する演算に持ち上げることで実現できます。ReaderTモナド変換子でのMonadTransクラスとMonadIOクラスのインスタンスの定義はそれぞれ以下の通りです。

instance MonadTrans (ReaderT r) where
    lift m = ReaderT $ \_ -> m

instance (MonadIO m) => MonadIO (ReaderT r m) where
    liftIO = lift . liftIO

 モナドmはReaderモナドではないので,liftやliftIOで持ち上げられる演算からはReaderTが保持する値rを直接参照する必要はありません。そのため,ワイルドカード・パターンを使って値rを切り捨てています。

 では実際に使ってみましょう。ReaderTモナド変換子をIOモナドに適用した「ReaderT IO」モナドを例に取ります。

 第15回で説明したように,GHCiのプロンプト上ではモナドmをIOモナドだと判断して実行するので,runReaderをrunReaderT,withReaderをwithReaderTにそれぞれ変更すれば,ReaderT IOモナドの実行結果を得ることができます。

Prelude Control.Monad.Reader> runReaderT ask "group"
"group"
Prelude Control.Monad.Reader> runReaderT (return () >> ask) "group"
"group"
Prelude Control.Monad.Reader> runReaderT (local ("semi-" ++) ask) "group"
"semi-group"
Prelude Control.Monad.Reader> runReaderT (local ("semi-" ++) (return () >> ask)) "group"
"semi-group"
Prelude Control.Monad.Reader> runReaderT (local ("semi-" ++) (return ()) >> ask) "group"
"group"
Prelude Control.Monad.Reader> runReaderT (withReaderT (const 0) ask) "group"
0
Prelude Control.Monad.Reader> runReaderT (withReaderT (const 0) (return () >> ask)) "group"
0

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

 次にliftIOを使い,ReaderTモナド変換子が保持する値を表示してみましょう。

module ReaderT where
import Control.Monad.Reader

ioTask :: Show r => ReaderT r IO ()
ioTask = do
    v <- ask
    liftIO $ print v

*ReaderT> runReaderT (return ()) "group"
*ReaderT> it
()
*ReaderT> runReaderT ioTask "group"
"group"
*ReaderT> runReaderT (local ("semi-" ++) ioTask) "group"
"semi-group"
*ReaderT> runReaderT (local ("semi-" ++) ioTask >> return 0) "group"
"semi-group"
0
*ReaderT> runReaderT (local ("semi-" ++) ioTask >> ioTask) "group"
"semi-group"
"group"
*ReaderT> runReaderT (withReaderT (const 0) ioTask) "group"
0
*ReaderT> runReaderT (withReaderT (const 0) ioTask >> ioTask) "group"
0
"group"
*ReaderT> runReaderT (withReaderT (const 0) ioTask >> ioTask >> return 0) "group"
0
"group"
0

 「現在のスコープにおいてaskが参照できる値」が表示されているのがわかります。

 さて,「askメソッドによって参照される値のスコープ」とは一体何でしょうか。

 上の例では,ioTaskを使う場所によって表示される値が異なっています。このように,askメソッドを使用する式は,Haskellの関数とは異なるスコープの変数束縛を持ちます。通常のHaskellの式では,変数束縛は式の記述時に静的に決定します。一方,askメソッドによって参照される値は,localメソッドやwithReader関数による影響を受けて動的に決定されるため,askメソッドを使って定義された変数は事実上,動的に束縛されることになります(参考リンク1参考リンク2)。

 これだけではわかりにくいかもしれません。上記の参考リンクにある例を少し改造したものを使って説明しましょう。

dynamicScope :: IO ()
dynamicScope = (flip runReaderT) "group" $ do
    ioTask
    local ("semi-" ++) ioTask
    thunk <- local (const "category") (return $ \() -> ioTask)
    thunk ()
    ioTask

 このdynamicScope関数を評価すると,以下のように出力されると思うかもしれません。

"group"
"semi-group"
"category"
"group"
【2009年5月4日追記】
当初,上の実行結果の最後の行が「"ioTask"」になっていましたが,正しくは「"group"」でした。現在は修正済みです。

 しかし,結果は異なります。式「local (const "category") (return $ \() -> ioTask)」で定義されたthunk関数は,関数を定義した部分の束縛である"category"ではなく,thunkを使用するdo式の束縛である"group"を使用するからです。実際には以下のように出力されます。

*ReaderT> dynamicScope
"group"
"semi-group"
"group"
"group"

 同様に,以下に定義する三つの関数の評価結果はすべて3になります(参考リンク)。

dynamicScope' :: IO Int
dynamicScope' = (flip runReaderT) 1 $ do
    x <- return $ \() -> do
         p <- ask
         return $ p+1
    local (const 2) $ x ()

dynamicScope'' :: IO Int
dynamicScope'' = (flip runReaderT) 1 $ do
    let x = \() -> do
            p <- ask
            return $ p+1
    local (const 2) $ x ()

dynamicScope''' :: IO Int
dynamicScope''' = (flip runReaderT) 1 $ do
    let x = \() -> do
            p <- ask
            return $ p+1
    y <- local (const 1) $ x ()
    local (const 2) $ x ()

*ReaderT> dynamicScope'
3
*ReaderT> dynamicScope''
3
*ReaderT> dynamicScope'''
3

 このように,ReaderモナドやReaderTモナド変換子は動的スコープを実現します。EmacsのようなエディタのバッファやWebアプリケーションのセッション管理など,局所的に他とは異なる状態を利用したい場合には,ReaderモナドやReaderTモナド変換子が役立つかもしれません。実際に,HackageDBで提供されているcgiパッケージのCGIモナドやCGITモナド変換子は,ReaderTモナド変換子を使った以下のような型として定義されています(参考リンク)。

-- | A simple CGI monad with just IO.
type CGI a = CGIT IO a

-- | The CGIT monad transformer.
newtype CGIT m a = CGIT { unCGIT :: ReaderT CGIRequest (WriterT Headers m) a }