3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

関数型プログラミング入門〜モナド変換子について①〜

Last updated at Posted at 2021-04-25

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モナド変換子がそれに当たります。

ReaderT
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モナド変換子以外にも、 MaybeTStateTなど、様々なモナド変換子を提供しています。これらを使えば、基盤となるモナドに対してMaybeモナドやStateモナドの振る舞いも行えるモナドを手に入れることができます。
便利な世の中ですね。それでは、今回はこの辺で。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?