Haskellなどの関数型プログラミングを学習していて、モナドのことが少しわかってきたかな、、と思っていたら次は「モナド変換子」という壁にぶち当たります。
オブジェクト指向に慣れ親しんだ我々平凡なプログラマに立ちはだかる第二の壁です。
Haskellでプログラムを書く場合、こいつを使わないとまともなプログラムを書くのが難しい。とってもじゃじゃ馬なやつですが、使えるととても便利なやつでもあります。
今回は「モナド変換子」の一側面を眺めて、モナド変換子の有難みを実感してみます。
モナド変換子導入のモチベーション
あまり厳密な言い方ではないかもしれませんが、実用上は「複数のモナドの効果をもつ計算を同時に行いたいから」と言って良いかと思います。
Monad型クラスの(>>=)の型注釈は以下のように定義されていました。
(>>=) :: Monad m => m a -> (a -> m b) -> m b
上記の型変数mはモナドですが、このmは全て同じモナドでなければなりません。
上記の「m a」はIO Int、「m b」はMaybe Stringのように、型変数mに対して、異なるモナドを適用するとコンパイルエラーとなります。
しかし、実際のプログラミングでは複数のモナドの演算を同じdo式内で使用したくなることがよくあります。
Haskellを始めたばかりの頃は、以下のようなコードでエラーを発生させた方は多いのではないでしょうか。
data AppConfig = AppConfig {
appName :: String
} deriving (Show)
appNameReader :: Reader AppConfig String
appNameReader = do
name <- asks appName
print name
return name
上記のコードをコンパイルしようとすると以下のようなエラーが発生します。
• Couldn't match type ‘IO’
with ‘ReaderT AppConfig Data.Functor.Identity.Identity’
Expected type: ReaderT AppConfig Data.Functor.Identity.Identity ()
Actual type: IO ()
• In a stmt of a 'do' block: print name
In the expression:
do name <- asks appName
print name
return name
In an equation for ‘appNameReader’:
appNameReader
= do name <- asks appName
print name
return name
関数appNameReaderは、型注釈としてReader AppConfig Stringが宣言されていますので、do式でのモナド演算は(Reader AppConfig)のモナド演算としなければなりません。
一方、関数printはprint :: Show a => a -> IO ()
というIOモナドの演算です。
(Reader AppConfig)とIOという異なるモナドによる演算をdo式に記述したためエラーとなっています。
このような問題は、モナド変換子を使用することで解決できます。
モナド変換子とは
以下は、Real World Haskell Chapter 18. Monad transformersの引用です。
A monad transformer is similar to a regular monad, but it's not a standalone entity: instead, it modifies the behaviour of an underlying monad.
「モナド変換子はそれ自体で独立するものではなく、基盤となるモナドの振る舞いを変えるものである」、といったことが書かれています。
非常に初心者泣かせな説明ですね。実用的には「複数のモナドを一つのモナドにまとめる変換子」という理解で十分だと思います。これにより、do式で複数のモナドの演算を扱えるようになります。
モナド変換子はtransformersパッケージのMonadTrans型クラスのインスタンスとして表現されます。
class MonadTrans (t :: (* -> *) -> * -> *) where
lift :: Monad m => m a -> t m a
上記の型変数tは、あるモナドmを受け取り、変換したモナド(t m)を生成するモナド変換子です。
また、liftはモナドmの計算を(t m)の文脈に持ち上げる効果を持つ関数です。
ここで注目したいのは(t m)もまたモナドである、ということです。
なので、liftの引数であるm a
のモナド演算を持ち上げた結果である(t m) a
もまたモナド演算です。
ということは、liftを使えば(t m)
モナド上で、mモナドの演算を行うことができるということです。
モナド変換子を使ってみる
先ほどのコンパイルエラーとなったコードを再度見てみましょう。
appNameReader :: Reader AppConfig String
appNameReader = do
name <- asks appName
print name
return name
appNameReader
内の演算はReader AppConfig
モナドの文脈である必要がありました。
異なるモナドの演算を行うためには、先ほど見たlift
を使って、IOモナドをReader AppConfig
のモナド(変換子)の文脈に持ち上げてやれば良いのです。
イメージとしては、以下のような感じです。
-- lift :: Monad m => m a -> t m a
lift :: IO () -> ((Reader AppConfigモナド変換子) IO) ()
モナド変換子は「基盤となるモナドの振る舞いを変えるもの」でした。Reader AppConfigモナド変換子 IO
で言う所の、IO
が基盤のモナドに当たります。これに対してモナド変換子を使って、Reader rモナドの効果を合わせ持つモナドを作り出せば良いのです。
ありがたいことに、transformersパッケージでは、Reader r
モナドのモナド変換子版を提供しています。ReaderT r
モナド変換子がそれに当たります。
newtype ReaderT r (m :: * -> *) a = ReaderT {runReaderT :: r -> m a}
instance (Monad m) => Monad (ReaderT r m) where
return = lift . return
m >>= k = ReaderT $ \ r -> do
a <- runReaderT m r
runReaderT (k a) r
instance MonadTrans (ReaderT r) where
lift = liftReaderT
liftReaderT :: m a -> ReaderT r m a
liftReaderT m = ReaderT (const m)
Reader r
モナドをReaderT r
モナド変換子に書き換えたコードサンプルが以下です。
appNameReader :: ReaderT AppConfig IO String
appNameReader = do
name <- asks appName -- (asks appName) :: (ReaderT AppConfig IO) String
lift . print $ name -- ( lift . print $ name) :: (ReaderT AppConfig IO) ()
return name
修正前のappNameReader
は、Reader AppConfig
のモナド演算でしたので、それ以外のモナド演算をdo式で使用することはできませんでした。
しかし、修正後はReaderT AppConfig IO
モナドとなっています。これは基盤となるIOモナドにReader rモナドの効果を合成したモナドとして振舞います。文字数が多いので読みにくいですが、ReaderT AppConfig IO
自体が一つのモナドとみなせるのです。これなら、IOモナドとReader rモナドの計算を、同じdo式内で行うことができます。
基盤となるIOモナドの演算を実行するときは、liftを使うのを忘れないようにしましょう。
lift . print $ name -- ( lift . print $ name) :: (ReaderT AppConfig IO) ()
print
はあくまでIOモナドの演算です。lift
を使って、ReaderT AppConfig IO
モナドの文脈に持ち上げてやる必要があります。
print "Hello" :: IO () -- IOモナドの演算なので、このままではReaderT AppConfig IOモナドのdo式内で使用できない
-- lift :: (MonadTrans t, Monad m) => m a -> t m a
lift . print $ "Hello" :: MonadTrans t => t IO () -- liftを使うと、あるモナド演算をMonadTrans(モナド変換子)の文脈に持ち上げることができる
上記の型変数tはモナド変換子です。サンプルコードでいうと、ReaderT AppConfig
にあたります。
型変数tにこれを当てはめると、以下のように読み替えられます。
lift . print $ "Hello" :: (ReaderT AppConfig IO) ()
liftを使うことにより、モナドの文脈を合わせてやることで、複数のモナドの演算を使用できることが分かりましたね。
最後に
今回は「モナド変換子」の基本的な概念を見てみました。
例として、ReaderT rモナド変換子を取り上げましたが、transformersパッケージにはReaderT rモナド変換子以外にも、 MaybeTやStateTなど、様々なモナド変換子を提供しています。これらを使えば、基盤となるモナドに対してMaybeモナドやStateモナドの振る舞いも行えるモナドを手に入れることができます。
便利な世の中ですね。それでは、今回はこの辺で。