Prev: モナドの合成は難しい TOC: 目次 Next: 標準モナド変換子

モナド変換子


モナド変換子はモナドの合成を容易にする、標準モナドの特別版です。 モナド変換子の型構築子はモナド型構築子に関してパラメータ化されており 合成されたモナドの型を生成します。

変換子の型構築子

型構築子は Haskell のモナドでは基盤としての役割をはたします。 Reader r a が 内部の型が a で、 r という型の環境をもつ Reader モナドの値の型であことを 思い出してください。型構築子 Reader r は、 Monad クラスのインスタンスで、 runReader::(r->a) 関数はその Readerモナドの計算を実行し、 型 a の結果を返します。

ReaderT とよばれるReader モナドの変換子版 が存在して、 追加パラメータとしてモナド型構築子が付け加わります。 ReaderT r m a は Reader が ベースのモナドで、m内部モナド であるような合成されたモナドの値の型です。 ReaderT r m はモナドクラスのインスタンスであり、 runReaderT::(r -> m a) 関数は、合成された モナドの計算を実行し、m a という型の結果を返します。

モナドの変換子版を使うと、簡単に合成モナドを生成できます。 ReaderT r IO は Reader と IO の合成モナドです。 また、変換子版モナドを恒等モナドに適用して、非変換子版のモナドを 生成することも可能です。つまり、 ReaderT r IdentityReader r と同じモナドになります。

もし、コードのコンパイル中に類エラーが出たら、それは型構築子が正しく 使われていないということです。正しい数のパラメータを型構築子に渡したか を確認し、複雑な型の式に括弧を付け忘れていないかを確かめてください。

もちあげ

モナド変換子で作った合成モナドを使うと、内部モナドの型の明示的な管理を しなくてすみます。その結果、わかりやすく、単純なコードになります。 その計算の内部で do ブロックを追加するかわりに、内部モナドの値を 処理するには、もちあげ演算を使って内部モナドから、合成モナドへ 関数を持ち出すことができます。

非モナド関数をモナドにもちあげるのに使う liftM 族の関数を 思い出してくさい。各変換子は、あるモナド計算を合成モナドへもちあげる関数 lift を提供します。多くの変換子は liftIO 関数も 提供しており、この関数は liftIO モナドの 計算をもちあげることに最適化したものです。実際の動きを見るために、 前の Continuation モナドの例をさらに発展させましょう。

example21.hs で使えるコード
fun :: IO String
fun = (`runContT` return) $ do
        n   <- liftIO (readLn::IO Int)
        str <- callCC $ \exit1 -> do     -- "exit1" の定義
          when (n < 10) (exit1 (show n))
          let ns = map digitToInt (show (n `div` 2))
          n' <- callCC $ \exit2 -> do    -- "exit2" の定義
            when ((length ns) < 3) (exit2 (length ns))
            when ((length ns) < 5) $ do liftIO $ putStrLn "Enter a number:"
                                        x <- liftIO (readLn::IO Int)
                                        exit2 x
            when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns)
                                        exit1 (dropWhile (=='0') ns') -- 2 レベル脱出
            return $ sum ns
          return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
        return $ "Answer: " ++ str

ContT を使ったこの関数、つまり Cont の 変換子版と、元々のバージョンのものとを比べると、モナド変換子を つかうと変更がほとんど目立たないことがわかります。

例 19 の入れ子になったモナド 例 21 の変換子を用いた合成モナド
fun = do n <- (readLn::IO Int)
         return $ (`runCont` id) $ do
           str <- callCC $ \exit1 -> do
             when (n < 10) (exit1 (show n))
             let ns = map digitToInt (show (n `div` 2))
             n' <- callCC $ \exit2 -> do
               when ((length ns) < 3) (exit2 (length ns))
               when ((length ns) < 5) (exit2 n)
               when ((length ns) < 7) $ do
                 let ns' = map intToDigit (reverse ns)
                 exit1 (dropWhile (=='0') ns')
               return $ sum ns
             return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
           return $ "Answer: " ++ str
fun = (`runContT` return) $ do
        n   <- liftIO (readLn::IO Int)
        str <- callCC $ \exit1 -> do
          when (n < 10) (exit1 (show n))
          let ns = map digitToInt (show (n `div` 2))
          n' <- callCC $ \exit2 -> do
            when ((length ns) < 3) (exit2 (length ns))
            when ((length ns) < 5) $ do
              liftIO $ putStrLn "Enter a number:"
              x <- liftIO (readLn::IO Int)
              exit2 x
            when ((length ns) < 7) $ do
              let ns' = map intToDigit (reverse ns)
              exit1 (dropWhile (=='0') ns')
            return $ sum ns
          return $ "(ns = " ++ (show ns) ++ ") " ++ (show n')
        return $ "Answer: " ++ str

計算の途中に入出力を追加する影響は、モナド変換子をつかうと狭い範囲に 閉じ込められます。これを、同じ結果を得るために、手で合成したモナドを 使う場合に必要な変更と比べてみて 下さい。


Prev: モナドの合成は難しい TOC: 目次 Next: 標準モナド変換子