今回は「モナド変換子 (monad transformer)」について説明します。バインド演算子 (>>=) や do 構文は、同じ文脈 (モナド) でなければ処理を連結することはできません。たとえば、I/O アクションで得られた値を Maybe の Just で包んで返すとか、Maybe の文脈で I/O アクションを行うことはできません。
ghci> do {a <- getLine; Just a} => エラー ghci> Just 10 >>= \x -> print x => エラー
このような場合、Maybe モナドと IO モナドの両方を扱うことができるモナドがあると便利です。Haskell の場合、「モナド変換子」を使うと二つのモナドを合成することができます。
モナド変換子は、あるモナド m を受け取って新しいモナドを返す型構築子のことです。たとえば、Maybe a に対応するモナド変換子 MaybeT を考えてみましょう。
データ型は MaybeT m a で、m が合成するモナドを表します。モナド変換子 MaybeT を使うと、Maybe モナドと IO モナドを合成した新しいモナド (MaybeT IO) を作ることができ、その文脈の中で I/O アクションを実行することができます。
ghci> :m + Control.Monad.Trans.Maybe ghci> :m + Control.Monad.Trans ghci> runMaybeT $ do {a <- lift(getLine); return a} hello, world Just "hello, world" ghci> runMaybeT $ return 10 >>= \x -> lift(print x) 10 Just ()
Maybe のモナド変換子 MaybeT は Haskell の標準ライブラリ Control.Monad.Trans.Maybe にあります。他の基本的なモナド (Either, List, Writer, Reader, State など) にも対応するモナド変換子 (ErrorT, ListT, WriterT, ReaderT, StateT など) が標準ライブラリに用意されています。
なお、GHC version 8.8.4 において、ErrorT が定義されているモジュール Control.Monad.Error の使用は非推奨になりました。かわりにモジュール Control.Monad.Except に定義されているモナド変換子 ExceptT を使います。本稿 (改訂版) でも ExceptT を使うこととし、ErrorT の説明は削除することにします。
同様に、ListT が定義されているモジュール Control.Monad.Trans.List の使用も非推奨になりました。さらに、ListT を使うとワーニング "This transformer is invalid on most monads" が表示されます。このため、本稿 (改訂版) では ListT の説明を削除することにします。あしからずご了承くださいませ。
モナド変換子 MaybeT は次のように定義されています。
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
m はモナド変換子 MaybeT が受け取るモナドです。MaybeT m a は m の中に Maybe a を格納するデータ構造、つまりモナド m で Maybe を包み込む形式になります。簡単に言えば、モナドを入れ子にするわけです。
次は、MaybeT をモナドのインスタンスにします。次のリストを見てください。
リスト : MaybeT の定義 instance Monad m => Monad (MaybeT m) where return x = MaybeT $ return (Just x) m >>= k = MaybeT $ do a <- runMaybeT m case a of Nothing -> return Nothing Just v -> runMaybeT (k v) fail _ = MaybeT $ return Nothing
GHC version 8.8.4 の場合、fail は Monad から分離されて Control.Monad.Fail.MonadFail に移されました。実際の定義は上記プログラムとは異なるので注意してください。MaybeT の動作はこれでも十分理解できると思います。
MaybeT は Maybe をモナド m で包んだものです。Maybe の return x は Just x なので、MaybeT の return x は Just x をモナド m で包めばよいことになります。右辺の return はモナド m の return のことで、右辺の文脈はモナド m であることに注意してください。
次はバインド演算子を定義します。runMaybeT で MaybeT の値を取り出して、do 構文の <- でモナド m の中身を取り出します。ここでも do 構文の文脈はモナド m であることに注意して下さい。
変数 a のデータ型は Maybe になるので、case で処理を振り分けます。Nothing であれば return Nothing を返します。Just v であれば値 v を関数 k に適用します。これで入れ子になったモナドから値を取り出して、それを関数 k に渡すことができます。
k の返り値のデータ型は MaybeT なので、この値をそのまま返すと MaybeT が入れ子になり、型の不一致でエラーになります。そこで、runMaybeT で MaybeT から値を取り出しています。
fail は引数を無視して Nothing をモナド m に包んで返すだけです。
それでは実行してみましょう。
ghci> :m + Control.Monad.Trans.Maybe ghci> runMaybeT $ (return 1 :: MaybeT [] Int) [Just 1] ghci> runMaybeT $ (return 1 :: MaybeT [] Int) >>= \x -> return (x * 2) [Just 2] ghci> runMaybeT $ (fail "" :: MaybeT [] Int) [Nothing] ghci> runMaybeT $ (fail "" :: MaybeT [] Int) >>= \x -> return (x * 2) [Nothing] ghci> runMaybeT $ (return 1 :: MaybeT IO Int) Just 1 ghci> runMaybeT $ (return 1 :: MaybeT IO Int) >>= \x -> return (x * 2) Just 2 ghci> runMaybeT $ (fail "" :: MaybeT IO Int) Nothing ghci> runMaybeT $ (fail "" :: MaybeT IO Int) >>= \x -> return (x * 2) Nothing
MaybeT [ ] Int はリストの中に Maybe Int が格納されます。MaybeT IO Int は IO モナドの中に Maybe Int が格納されます。どちらの場合も、バインド演算子 (>>=) は Maybe の中のデータを取り出して、それを次の関数に渡します。Maybe が Nothing であれば、それ以降の処理も Nothing になります。
MaybeT は Functor のインスタンスにすることもできます。次のリストを見てください。
リスト : Functor の定義 instance Monad m => Functor (MaybeT m) where fmap f x = MaybeT $ do a <- runMaybeT x case a of Nothing -> return Nothing Just y -> return (Just (f y))
fmap は MaybeT が格納している値に関数 f を適用します。最初に、runMaybeT で x から値を取り出して変数 a にセットし、case で処理を振り分けます。あとは、Maybe の Functor と同じ考え方です。a が Noting であれば、Nothing をモナド m に包んで返します。Just y であれば、値 y に関数 f を適用して、その返り値を Just とモナド m に包んで返します。
簡単な実行例を示します。
ghci> runMaybeT $ fmap (*2) (return 1 :: MaybeT [] Int) [Just 2] ghci> runMaybeT $ fmap (*2) (return 1 :: MaybeT IO Int) Just 2 ghci> runMaybeT $ fmap (*2) (return 1 :: MaybeT Maybe Int) Just (Just 2) ghci> runMaybeT $ fmap (*2) (fail "" :: MaybeT [] Int) [Nothing] ghci> runMaybeT $ fmap (*2) (fail "" :: MaybeT IO Int) Nothing ghci> runMaybeT $ fmap (*2) (fail "" :: MaybeT Maybe Int) Just Nothing
fmap は MaybeT の値に関数 f を適用して、その結果を MaybeT に格納して返していることがわかります。
MaybeT を使うと Maybe とモナド m を合成することができますが、生成されるモナドは MaybeT m という型になるので、モナド m のモナド関数を MaybeT m の文脈でそのまま使うことはできません。次の例を見てください。
ghci> (return 1 :: MaybeT IO Int) >>= \x -> print x => エラー
print x の返り値の型は IO () です。バインド演算子の文脈は MaybeT IO なので、型が不一致になるためエラーとなります。この場合、print を MaybeT IO に持ち上げる処理が必要になります。この処理を関数 lift で行います。
lift はモジュール Control.Monad.Trans のクラス MonadTrans に定義されています。
リスト : MonadTrans クラスの定義 class MonadTrans t where lift :: (Monad m) => m a -> t m a
t はモナド変換子、m はモナドです。lift は m a を受け取って、t m a を返します。モナド m の処理を t m という合成したモナドに持ち上げていることがわかります。
MaybeT の lift 関数は次のようになります。
リスト : MaybeT の lift 関数 instance MonadTrans MaybeT where lift m = MaybeT $ m >>= (\x -> return (Just x))
lift の引数 m はモナドです。バインド演算子でモナドから値を取り出し、その値を Just と return で包めばいいわけです。ラムダ式の代わりに関数合成を使って m >>= return . Just としてもかまいません。
簡単な実行例を示します。
ghci> :m + Control.Monad.Trans ghci> runMaybeT $ (return 1 :: MaybeT IO Int) >>= lift . print 1 Just () ghci> runMaybeT $ (return 1 :: MaybeT IO Int) >>= \x -> do {lift(print x); return (x * 2)} 1 Just 2
最初の例は print の返り値が IO () なので、ユニット () が IO と Just に格納されて返されます。次の例は do 構文の中で x の値を print で表示しています。このように、モナド変換子を使って IO モナドを合成すると、計算途中の変数値を表示することができます。
なお、I/O アクションを持ち上げる専用の関数 liftIO がモジュール Control.Monad.IO.Class に用意されています。
リスト : liftIO の定義 class (Monad m) => MonadIO m where liftIO :: IO a -> m a instance MonadIO IO where liftIO = id instance MonadIO m => MonadIO (MaybeT m) where liftIO = lift . liftIO
M.Hiroi は勉強不足でよくわかりませんが、IO モナドに限定することで、コンパイルする時に最適化が行われる (期待できる) のかもしれません。
簡単な実行例を示します。
ghci> :m + Control.Monad.IO.Class ghci> runMaybeT $ (return 1 :: MaybeT IO Int) >>= liftIO . print 1 Just () ghci> runMaybeT $ (return 1 :: MaybeT IO Int) >>= \x -> do {liftIO(print x); return (x * 2)} 1 Just 2
lift のかわりに liftIO を使っただけなので、結果は lift と同じになります。
なお、参考 URL 2「本物のプログラムは Haskell を使う」"第 15 回 Haskellでのデバッグのコツをつかむ" によると、『liftメソッドの定義は、モナド変換子則(monad transformer law)と呼ばれる以下の二つの法則を満たすように定義される必要があります。』 とのことです。
モナド変換子は MonadPlus のインスタンスに設定しておくと便利なことがあります。この場合、二通りの方法が考えられます。ひとつはモナド変換子 t の元になるモナドの MonadPlus に合わせる方法、もう一つはモナド変換子 t の引数に与えられるモナド m の MonadPlus に合わせる方法です。MaybeT m でいえば、Maybe の MonadPlus に従うか、モナド m の MonadPlus に従うかということです。
一般に、モナドは失敗系 (Maybe, Either, List) と状態系 (Writer, Reader, State, IO) の二つに大別することができます。Haskell の標準ライブラリにあるモナド変換子のソースをみると、失敗系のモナド変換子の MonadPlus は元になるモナドに、状態系の場合は引数として与えられるモナドにあわせているようです。
MaybeT は失敗系のモナド変換子なので、MoandPlus のプログラムは次のようになります。
リスト : MonadPlus の定義 instance Monad m => MonadPlus (MaybeT m) where mzero = MaybeT $ return Nothing x `mplus` y = MaybeT $ do a <- runMaybeT x case a of Nothing -> runMaybeT y Just _ -> return a
動作は Maybe の MonadPlus とほぼ同じです。mzero は Nothing をモナド m に包んで返します。mplus も Maybe とほぼ同じで、左辺が Just であればその値を返し、Nothing であれば右辺の値を返します。これで mplus は Maybe のそれと同様の動作になります。
簡単な実行例を示します。
ghci> runMaybeT $ (return 1 :: MaybeT [] Int) `mplus` return 2 [Just 1] ghci> runMaybeT $ (mzero :: MaybeT [] Int) `mplus` return 2 [Just 2] ghci> runMaybeT $ (mzero :: MaybeT [] Int) `mplus` mzero [Nothing]
それでは、MaybeT を使った簡単なプログラムを作ってみましょう。次のリストを見てください。
リスト : MaybeT の例題 import Control.Monad import Control.Monad.Trans import Control.Monad.Trans.Maybe type MaybeIO a = MaybeT IO a getWord :: MaybeIO String getWord = do lift(putStr "Input> ") a <- lift (getLine) when (a == "") (fail "") return a test01 :: MaybeIO String test01 = do a <- getWord b <- getWord return (a ++ b) test01' :: IO () test01' = do a <- runMaybeT test01 case a of Nothing -> return () Just s -> do {putStrLn s; test01'} test02 :: MaybeIO String test02 = getWord `mplus` getWord `mplus` getWord
最初に type で MaybeT IO a に MaybeIO a という別名をつけます。関数 getWord は getLine で標準入力から 1 行読み込み、それを MaybeIO に格納して返します。ただし、空文字列の場合は fail を呼び出して失敗します。
test01 は getWord で標準入力から 2 行読み込み、それらを連結した文字列を返します。test01' は test01 を繰り返し呼び出します。test01 が失敗したら繰り返しを終了します。runMaybeT test01 でモナドから値を取り出します。このデータ型は Maybe なので、case で場合分けします。Nothing であれば return () を返し、そうでなければ文字列 s を表示して test01' を再帰呼び出しするだけです。
test02 は mplus のテストです。getWord を 3 回呼び出しますが、そのうちの 1 回でも入力があれば、その値を返します。
それでは実行してみましょう。
ghci> runMaybeT getWord Input> hello, world Just "hello, world" ghci> runMaybeT test01 Input> hello, Input> world Just "hello, world" ghci> runMaybeT getWord Input> Nothing ghci> runMaybeT test01 Input> hello, Input> Nothing ghci> test01' Input> hello, Input> world hello, world Input> foo bar Input> baz foo bar baz Input> ghci> ghci> runMaybeT test02 Input> hello, world Just "hello, world" ghci> runMaybeT test02 Input> Input> hello, world Just "hello, world" ghci> runMaybeT test02 Input> Input> Input> hello, world Just "hello, world" ghci> runMaybeT test02 Input> Input> Input> Nothing
正常に動作していますね。
次は Either モナドのモナド変換子 ExceptT を説明します。ExceptT はモジュール Control.Monad.Except に定義されています。ExceptT のデータ型は ExceptT e m a で、e は Either の Left に格納されるデータ型、m がモナド、a は Either の Right に格納されるデータ型を表します。
それから、Control.Monad.Except にあるモナド MonadError には throwError と catchError が定義されていて、エラーの送出は fail ではなく throwError を使います。また、catchError を使って throwError が送出したエラーを捕捉することができます。
簡単な実行例を示しましょう。
ghci> :m + Control.Monad.Except ghci> runExceptT (return 1 :: ExceptT String [] Int) [Right 1] ghci> runExceptT $ (return 1 :: ExceptT String [] Int) >>= \x -> return (x * 2) [Right 2] ghci> runExceptT (return 1 :: ExceptT String IO Int) Right 1 ghci> runExceptT $ (return 1 :: ExceptT String IO Int) >>= \x -> return (x * 2) Right 2 ghci> runExceptT $ (throwError "oops" :: ExceptT String [] Int) >>= \x -> return (x * 2) [Left "oops"] ghci> runExceptT $ (throwError "oops" :: ExceptT String IO Int) >>= \x -> return (x * 2) Left "oops" ghci> runExceptT $ (return 1 :: ExceptT String Maybe Int) `mplus` (return 2) Just (Right 1) ghci> runExceptT $ (mzero :: ExceptT String Maybe Int) `mplus` (return 2) Just (Right 2) ghci> runExceptT $ (mzero :: ExceptT String Maybe Int) `mplus` mzero Just (Left "") ghci> runExceptT $ (return 1 :: ExceptT String [] Int) `mplus` (return 2) [Right 1] ghci> runExceptT $ (mzero :: ExceptT String [] Int) `mplus` (return 2) [Right 2] ghci> runExceptT $ (mzero :: ExceptT String [] Int) `mplus` mzero [Left ""] ghci> runExceptT $ (return 1 :: ExceptT String IO Int) `catchError` \e -> do {liftIO(print e); return 0} Right 1 ghci> runExceptT $ (throwError "" :: ExceptT String IO Int) `catchError` \e -> do {liftIO(print e); return 0} "" Right 0 ghci> runExceptT $ (throwError "oops" :: ExceptT String IO Int) `catchError` \e -> do {liftIO(print e); return 0} "oops" Right 0 ghci> runExceptT $ (throwError "oops" :: ExceptT String IO Int) `catchError` \e -> do {liftIO(print e); throwError e} "oops" Left "oops"
MaybeT とは違って、ExceptT はエラー情報を伝えることができます。また、throwError と catchError を使って、モナド変換子でもエラーを簡単に扱うことができます。
ExceptT でも Functor, lift, MonadPlus を使用することができます。簡単な実行例を示しましょう。
ghci> runExceptT $ fmap (*2) (return 1 :: ExceptT String [] Int) [Right 2] ghci> runExceptT $ fmap (*2) (return 1 :: ExceptT String IO Int) Right 2 ghci> runExceptT $ fmap (*2) (throwError "oops" :: ExceptT String [] Int) [Left "oops"] ghci> runExceptT $ fmap (*2) (throwError "oops" :: ExceptT String IO Int) Left "oops" ghci> runExceptT $ (return 1 :: ExceptT String IO Int) >>= \x -> do {lift(print x); return (x * 2)} 1 Right 2 ghci> runExceptT $ (return 1 :: ExceptT String IO Int) >>= \x -> do {liftIO(print x); return (x * 2)} 1 Right 2 ghci> runExceptT $ (return 1 :: ExceptT String IO Int) `mplus` (return 2) Right 1 ghci> runExceptT $ (mzero :: ExceptT String IO Int) `mplus` (return 2) Right 2 ghci> runExceptT $ (mzero :: ExceptT String IO Int) `mplus` mzero Left ""
それでは ExceptT の簡単な例題として、MaybeT の例題を ExceptT で書き直してみましょう。次のリストを見てください。
リスト : ExceptT の簡単な例題 import Control.Monad.Except type ErrorIO a = ExceptT String IO a getWord' :: ErrorIO String getWord' = do lift(putStr "Input> ") a <- lift (getLine) when (a == "") (throwError "empty string") return a test03 :: ErrorIO String test03 = do a <- getWord' b <- getWord' return (a ++ b) test03' :: IO () test03' = do a <- runExceptT test03 case a of Left s -> putStrLn s Right s -> do {putStrLn s; test03'} test04 :: ErrorIO String test04 = getWord' `mplus` getWord' `mplus` getWord'
最初に type で ExceptT String IO a に ErrorIO a という別名をつけます。関数 getWord' は getLine で標準入力から 1 行読み込み、それを ErrorIO に格納して返します。ただし、空文字列の場合は throwError を呼び出して失敗します。このとき、文字列 "empty string" を渡します。
test03 は getWord' で標準入力から 2 行読み込み、それらを連結した文字列を返します。test03' は test03 を繰り返し呼び出します。test03 が失敗したら繰り返しを終了します。runExceptT test03 でモナドから値を取り出し、case で場合分けします。Left s であれば文字列 s を putStrLn で表示します。そうでなければ Right の文字列 s を表示して test03' を再帰呼び出しするだけです。
test04 は mplus のテストです。getWord' を 3 回呼び出しますが、そのうちの 1 回でも入力があれば、その値を返します。
それでは実行してみましょう。
ghci> runExceptT getWord' Input> hello, world Right "hello, world" ghci> runExceptT getWord' Input> Left "empty string" ghci> runExceptT test03 Input> hello, Input> world Right "hello, world" ghci> runExceptT test03 Input> hello, Input> Left "empty string" ghci> runExceptT test03 Input> Left "empty string" ghci> runExceptT test03 Input> Left "empty string" ghci> test03' Input> hello, Input> world hello, world Input> foo bar Input> baz foo bar baz Input> empty string ghci> runExceptT test04 Input> hello, wordl Right "hello, wordl" ghci> runExceptT test04 Input> Input> hello, world Right "hello, world" ghci> runExceptT test04 Input> Input> Input> hello, world Right "hello, world" ghci> runExceptT test04 Input> Input> Input> Left "empty string"
モジュール Control.Monad.Identity に定義されている Identity モナド (恒等モナド) を使うと、モナド変換子から元のモナドを生成することができます。Identity モナド は次のように定義されています。
リスト : 恒等モナド newtype Identity a = Identity {runIdentity :: a} instance Monad Identity where return x = Identity x m >>= k = k (runIdentity m)
return x は x をデータ構築子 Identity に格納するだけ、バインド演算子は runIdentity m でデータを取り出して、その値に関数 k を適用するだけです。
簡単な例を示しましょう。Maybe と同じ動作をする Maybe' を定義します。
リスト : Maybe' の定義 type Maybe' a = MaybeT Identity a runMaybe' = runIdentity . runMaybeT
type で MaybeT Identity a の別名 Maybe' を定義します。runMaybe' は MaybeT の中にある Identity の値を取り出すので、関数合成して runIdentity . runMaybeT となります。
簡単な実行例を示します。
ghci> runMaybe' $ fmap (*2) (return 1) Just 2 ghci> runMaybe' $ return 1 Just 1 ghci> runMaybe' $ return 1 >>= \x -> return (x * 2) Just 2 ghci> runMaybe' $ fail "" >>= \x -> return (x * 2) Nothing ghci> runMaybe' $ fmap (*2) (return 10) Just 20 ghci> runMaybe' $ fmap (*2) (fail "") Nothing ghci> runMaybe' $ return 1 `mplus` return 2 Just 1 ghci> runMaybe' $ mzero `mplus` return 2 Just 2 ghci> runMaybe' $ mzero `mplus` mzero Nothing
Maybe' は Maybe と同じ動作になっていることがわかると思います。
今回はここまでです。次回は状態系のモナド変換子 WriteT, ReaderT, StateT について説明します。