M.Hiroi's Home Page

Functional Programming

お気楽 Haskell プログラミング入門

[ PrevPage | Haskell | NextPage ]

モナド変換子

今回は「モナド変換子 (monad transformer)」について説明します。バインド演算子 (>>=) や do 構文は、同じ文脈 (モナド) でなければ処理を連結することはできません。たとえば、I/O アクションで得られた値を Maybe の Just で包んで返すとか、Maybe の文脈で I/O アクションを行うことはできません。

Prelude> do {a <- getLine; Just a}
=> エラー

Prelude> Just 10 >>= \x -> print x
=> エラー

このような場合、Maybe モナドと IO モナドの両方を扱うことができるモナドがあると便利です。Haskell の場合、「モナド変換子」を使うと二つのモナドを合成することができます。

●モナド変換子とは?

モナド変換子は、あるモナド m を受け取って新しいモナドを返す型構築子のことです。たとえば、Maybe a に対応するモナド変換子 MaybeT を考えてみましょう。データ型は MaybeT m a で、m が合成するモナドを表します。モナド変換子 MaybeT を使うと、Maybe モナドと IO モナドを合成した新しいモナド (MaybeT IO) を作ることができ、その文脈の中で I/O アクションを実行することができます。

Prelude> :m + Control.Monad.Trans.Maybe
Prelude Control.Monad.Trans.Maybe> :m + Control.Monad.Trans
Prelude ...> runMaybeT $ do {a <- lift(getLine); return a}
hello, world
Just "hello, world"
Prelude ...> 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

モナド変換子 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 に包んで返すだけです。

それでは実行してみましょう。

Prelude> :m + Control.Monad.Trans.Maybe
Prelude ...> runMaybeT $ (return 1 :: MaybeT [] Int)
[Just 1]
Prelude ...> runMaybeT $ (return 1 :: MaybeT [] Int) >>= \x -> return (x * 2)
[Just 2]
Prelude ...> runMaybeT $ (fail "" :: MaybeT [] Int)
[Nothing]
Prelude ...> runMaybeT $ (fail "" :: MaybeT [] Int) >>= \x -> return (x * 2)
[Nothing]

Prelude ...> runMaybeT $ (return 1 :: MaybeT IO Int)
Just 1
Prelude ...> runMaybeT $ (return 1 :: MaybeT IO Int) >>= \x -> return (x * 2)
Just 2
Prelude ...> runMaybeT $ (fail "" :: MaybeT IO Int)
Nothing
Prelude ...> runMaybeT $ (fail "" :: MaybeT IO Int) >>= \x -> return (x * 2)
Nothing

MaybeT [ ] Int はリストの中に Maybe Int が格納されます。MaybeT IO Int は IO モナドの中に Maybe Int が格納されます。どちらの場合も、バインド演算子 (>>=) は Maybe の中のデータを取り出して、それを次の関数に渡します。Maybe が Nothing であれば、それ以降の処理も Nothing になります。

●Functor の定義

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 に包んで返します。

簡単な実行例を示します。

Prelude ...> runMaybeT $ fmap (*2) (return 1 :: MaybeT [] Int)
[Just 2]
Prelude ...> runMaybeT $ fmap (*2) (return 1 :: MaybeT IO Int)
Just 2
Prelude ...> runMaybeT $ fmap (*2) (return 1 :: MaybeT Maybe Int)
Just (Just 2)
Prelude ...> runMaybeT $ fmap (*2) (fail "" :: MaybeT [] Int)
[Nothing]
Prelude ...> runMaybeT $ fmap (*2) (fail "" :: MaybeT IO Int)
Nothing
Prelude ...> runMaybeT $ fmap (*2) (fail "" :: MaybeT Maybe Int)
Just Nothing

fmap は MaybeT の値に関数 f を適用して、その結果を MaybeT に格納して返していることがわかります。

●lift 関数

MaybeT を使うと Maybe とモナド m を合成することができますが、生成されるモナドは MaybeT m という型になるので、モナド m のモナド関数を MaybeT m の文脈でそのまま使うことはできません。次の例を見てください。

Prelude Control.Monad.Trans.Maybe> (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 としてもかまいません。

簡単な実行例を示します。

Prelude Control.Monad.Trans.Maybe> :m + Control.Monad.Trans
Prelude ...> runMaybeT $ (return 1 :: MaybeT IO Int) >>= lift . print
1
Just ()
Prelude ...> 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 モナドに限定することで、コンパイルする時に最適化が行われる (期待できる) のかもしれません。

簡単な実行例を示します。

Prelude ...> :m + Control.Monad.IO.Class
Prelude ...> runMaybeT $ (return 1 :: MaybeT IO Int) >>= liftIO . print
1
Just ()
Prelude ...> runMaybeT $ (return 1 :: MaybeT IO Int) >>= \x -> do {liftIO(print x); return (x * 2)}
1
Just 2

lift のかわりに liftIO を使っただけなので、結果は lift と同じになります。

なお、参考 URL 2 によると、『liftメソッドの定義は、モナド変換子則(monad transformer law)と呼ばれる以下の二つの法則を満たすように定義される必要があります。』 とのことです。

  1. lift . return == return
  2. lift (m >>= k) == lift m >>= (lift . k)

●MonadPlus の定義

モナド変換子は 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 のそれと同様の動作になります。

簡単な実行例を示します。

Prelude ...> runMaybeT $ (return 1 :: MaybeT [] Int) `mplus` return 2
[Just 1]
Prelude ...> runMaybeT $ (mzero :: MaybeT [] Int) `mplus` return 2
[Just 2]
Prelude ...> runMaybeT $ (mzero :: MaybeT [] Int) `mplus` mzero
[Nothing]

●MaybeT の簡単な例題

それでは、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 回でも入力があれば、その値を返します。

それでは実行してみましょう。

*Main> runMaybeT getWord
Input> hello, world
Just "hello, world"
*Main> runMaybeT test01
Input> hello,
Input> world
Just "hello, world"
*Main> runMaybeT getWord
Input>
Nothing
*Main> runMaybeT test01
Input> hello,
Input>
Nothing
*Main> test01'
Input> hello,
Input> world
hello, world
Input> foo bar 
Input> baz
foo bar baz
Input>
*Main>

*Main> runMaybeT test02
Input> hello, world
Just "hello, world"
*Main> runMaybeT test02
Input> 
Input> hello, world
Just "hello, world"
*Main> runMaybeT test02
Input> 
Input> 
Input> hello, world
Just "hello, world"
*Main> runMaybeT test02
Input> 
Input> 
Input> 
Nothing

正常に動作していますね。

●ExceptT と MonadError

次は 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 が送出したエラーを捕捉することができます。

簡単な実行例を示しましょう。

Prelude> :m + Control.Monad.Except
Prelude ...> runExceptT (return 1 :: ExceptT String [] Int)
[Right 1]
Prelude ...> runExceptT $ (return 1 :: ExceptT String [] Int) >>= \x -> return (x * 2)
[Right 2]
Prelude ...> runExceptT (return 1 :: ExceptT String IO Int)
Right 1
Prelude ...> runExceptT $ (return 1 :: ExceptT String IO Int) >>= \x -> return (x * 2)
Right 2

Prelude ...> runExceptT $ (throwError "oops" :: ExceptT String [] Int) >>= \x -> return (x * 2)
[Left "oops"]
Prelude ...> runExceptT $ (throwError "oops" :: ExceptT String IO Int) >>= \x -> return (x * 2)
Left "oops"

Prelude ...> runExceptT $ (return 1 :: ExceptT String Maybe Int) `mplus` (return 2)
Just (Right 1)
Prelude ...> runExceptT $ (mzero :: ExceptT String Maybe Int) `mplus` (return 2)
Just (Right 2)
Prelude ...> runExceptT $ (mzero :: ExceptT String Maybe Int) `mplus` mzero
Just (Left "")

Prelude ...> runExceptT $ (return 1 :: ExceptT String [] Int) `mplus` (return 2)
[Right 1]
Prelude ...> runExceptT $ (mzero :: ExceptT String [] Int) `mplus` (return 2)
[Right 2]
Prelude ...> runExceptT $ (mzero :: ExceptT String [] Int) `mplus` mzero
[Left ""]
Prelude ...> runExceptT $ (return 1 :: ExceptT String IO Int) `catchError` 
\e -> do {liftIO(print e); return 0}
Right 1

Prelude ...> runExceptT $ (throwError "" :: ExceptT String IO Int) `catchError` 
\e -> do {liftIO(print e); return 0}
""
Right 0
Prelude ...> runExceptT $ (throwError "oops" :: ExceptT String IO Int) `catchError` 
\e -> do {liftIO(print e); return 0}
"oops"
Right 0
Prelude ...> 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

ExceptT でも Functor, lift, MonadPlus を使用することができます。簡単な実行例を示しましょう。

Prelude ...> runExceptT $ fmap (*2) (return 1 :: ExceptT String [] Int)
[Right 2]
Prelude ...> runExceptT $ fmap (*2) (return 1 :: ExceptT String IO Int)
Right 2
Prelude ...> runExceptT $ fmap (*2) (throwError "oops" :: ExceptT String [] Int)
[Left "oops"]
Prelude ...> runExceptT $ fmap (*2) (throwError "oops" :: ExceptT String IO Int)
Left "oops"

Prelude ...> runExceptT $ (return 1 :: ExceptT String IO Int) >>= \x -> do {lift(print x); return (x * 2)}
1
Right 2
Prelude ...> runExceptT $ (return 1 :: ExceptT String IO Int) >>= \x -> do {liftIO(print x); return (x * 2)}
1
Right 2

Prelude ...> runExceptT $ (return 1 :: ExceptT String IO Int) `mplus` (return 2)
Right 1
Prelude ...> runExceptT $ (mzero :: ExceptT String IO Int) `mplus` (return 2)
Right 2
Prelude ...> runExceptT $ (mzero :: ExceptT String IO Int) `mplus` mzero
Left ""

●ExceptT の簡単な例題

それでは 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 回でも入力があれば、その値を返します。

それでは実行してみましょう。

*Main> runExceptT getWord'
Input> hello, world
Right "hello, world"
*Main> runExceptT getWord'
Input>
Left "empty string"
*Main> runExceptT test03
Input> hello,
Input> world
Right "hello, world"
*Main> runExceptT test03
Input> hello,
Input>
Left "empty string"
*Main> runExceptT test03
Input>
Left "empty string"
*Main> runExceptT test03
Input>
Left "empty string"

*Main> test03'
Input> hello,
Input> world
hello, world
Input> foo bar
Input> baz
foo bar baz
Input>
empty string

*Main> runExceptT test04
Input> hello, wordl
Right "hello, wordl"
*Main> runExceptT test04
Input>
Input> hello, world
Right "hello, world"
*Main> runExceptT test04
Input>
Input>
Input> hello, world
Right "hello, world"
*Main> runExceptT test04
Input>
Input>
Input>
Left "empty string"

●Identity モナド

モジュール 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 となります。

簡単な実行例を示します。

Prelude ...> runMaybe' $ fmap (*2) (return 1)
Just 2
Prelude ...> runMaybe' $ return 1
Just 1
Prelude ...> runMaybe' $ return 1 >>= \x -> return (x * 2)
Just 2
Prelude ...> runMaybe' $ fail "" >>= \x -> return (x * 2)
Nothing
Prelude ...> runMaybe' $ fmap (*2) (return 10)
Just 20
Prelude ...> runMaybe' $ fmap (*2) (fail "")
Nothing
Prelude ...> runMaybe' $ return 1 `mplus` return 2
Just 1
Prelude ...> runMaybe' $ mzero `mplus` return 2
Just 2
Prelude ...> runMaybe' $ mzero `mplus` mzero
Nothing

Maybe' は Maybe と同じ動作になっていることがわかると思います。

今回はここまでです。次回は状態系のモナド変換子 WriteT, ReaderT, StateT について説明します。

●参考 URL

  1. WWW.SAMPOU.ORG, モナドのすべて
  2. 本物のプログラムは Haskell を使う, 第 15 回 Haskellでのデバッグのコツをつかむ

初版 2013 年 7 月 7 日
改訂 2021 年 7 月 25 日

Copyright (C) 2013-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Haskell | NextPage ]