順を追って理解する MaybeT モナドの使い方
モナド変換子の1つである MaybeT について、順を追って説明してみたいと思います。
想定読者
- モナドや Applicative スタイルを理解している。
- モナド変換子について学び始めたので具体例を知りたい。
Maybeモナドの復習
2つの文字列を受け取り、両方とも数値の場合はそれらを足した数、そうでない場合はNothing
を返す関数 add
は以下のように実装できます。
import Text.Read (readMaybe)
readInt :: String -> Maybe Int
readInt = readMaybe
-- |
-- >>> add "1" "2"
-- Just 3
-- >>> add "1" "x"
-- Nothing
--
add :: String -> String -> Maybe Int
add sa sb = do
a <- readInt sa
b <- readInt sb
return $ a + b
Maybe モナドにより失敗を上手に扱えているのが分かります。
なお、do 記法を使わずに書くと以下のようになります。
add :: String -> String -> Maybe Int
add sa sb =
readInt sa >>= \a ->
readInt sb >>= \b ->
return $ a + b
また、Applicative スタイルでは以下のように書けます(おそらく今回のケースではこれが最もシンプルでしょう)。
add :: String -> String -> Maybe Int
add sa sb = (+) <$> readInt sa <*> readInt sb
IO から読み取った文字列を対象にする
さて、今度は文字列をコンソールから入力するようにした addIO
関数を考えてみます。
文字列の読み込みは getLine :: IO String
が使えるので、それに fmap readLine
を適用する形で以下のように書きたいところですが、残念ながらコンパイルエラーになります。
addIO :: IO (Maybe Int)
addIO = do
a <- fmap readInt getLine
b <- fmap readInt getLine
return $ a + b -- コンパイルエラー
IO を扱う必要があるので IO モナドを利用することになりますが、同時に Maybe モナドを利用できないためです。
addIO :: IO (Maybe Int) -- IOモナドを利用している
addIO = do
a <- fmap readInt getLine -- a :: Maybe Int
b <- fmap readInt getLine -- b :: Maybe Int
return $ a + b -- `Maybe Int` 同士は足し合わせられない
Maybe モナドを利用した場合は a
や b
は Int
型になるため問題ありませんが、IO モナドを利用しているため Maybe Int
型になってしまうのが原因です。
愚直な対処方法
これに対処する方法としてはいくつかの方法が考えられます。
関数を用意する
最も愚直な方法は、Maybe Int
同士を足し合わせる関数を用意してしまうことです。
addIO :: IO (Maybe Int)
addIO = do
ma <- fmap readInt getLine
mb <- fmap readInt getLine
return $ f ma mb
where
f :: Maybe Int -> Maybe Int -> Maybe Int
f ma mb = do -- Maybe モナドを利用
a <- ma
b <- mb
return (a + b)
新たに定義した関数 f
は IO モナドの do 式から独立しているため、Maybe モナドを利用した処理を書くことが可能になっています。なんだか大げさではありますが、とりあえず動きます。
Applicative スタイルで処理する
関数を用意する代わりに return
の箇所で、Applicative スタイルを利用することもできます。
addIO :: IO (Maybe Int)
addIO = do
ma <- fmap readInt getLine
mb <- fmap readInt getLine
return $ (+) <$> ma <*> mb
これも関数を定義するやり方と考え方は同じで、do 式とは独立した return
の部分で処理してしまおうという考え方です。
MaybeT モナド
ここで本題ですが、Maybe モナドと別のモナド(IOモナドとか)を合体させた新しいモナドを作ってしまおうという考え方があります。
それがいわゆる「モナド変換子」と呼ばれるもので、transformers
パッケージに各種モナド変換子が用意されています。Stack を利用している場合には package.yaml
に追記しておきましょう。
dependencies:
- base >= 4.7 && < 5
+- transformers
Maybe モナドと別のモナドを合体させることができる MaybeT
モナドを利用すると以下のように書くことができます。
import Control.Monad.Trans.Maybe ( MaybeT(..) )
addIO :: MaybeT IO Int
addIO = do
a <- MaybeT $ fmap readInt getLine -- a :: Int
b <- MaybeT $ fmap readInt getLine -- b :: Int
return $ a + b
IO 処理を伴いつつも、Maybe モナドの機能を同時に利用できているのが分かります。
解説
順に見ていくと、まず MaybeT IO
というモナドが利用されています。
addIO :: MaybeT IO Int
これは MaybeT
が IO
モナドを包み込んでいるようなイメージです。そして、モナドとして扱う値が Int
なので MaybeT IO Int
という型になっているわけですね。
次に MaybeT
で fmap readInt getLine :: IO (Maybe Int)
を包んでいるのが分かります。
a <- MaybeT $ fmap readInt getLine -- a :: Int
MaybeT
は m (Maybe a) -> MaybeT m a
と定義されており、ここでは IO (Maybe Int)
から MaybeT IO Int
に変換しているのが分かります。この do 式で扱っているのは Maybe IO Int
型なので、IO
モナドをラップしているわけですね。
そして <-
で取り出した値は Int
になっているため、return (a + b)
と通常の Maybe モナドと同じように計算できているのが分かります。
return $ a + b
do 記法を使わずに書くと
理解のために do 記法を使わずに書くと以下のようになります。
addIO :: MaybeT IO Int
addIO =
MaybeT (fmap readInt getLine) >>= \a ->
MaybeT (fmap readInt getLine) >>= \b ->
return $ a + b
MaybeT
が IO
モナドを包み込みつつ、Maybe
モナドと同等の振る舞いがされていることが分かりやすいのではないでしょうか。
lift 関数
なお、 IO (Maybe a)
ではなく、通常の IO a
型を MaybeT
モナドに持ち上げるために lift
という関数が利用できます。
import Control.Monad.Trans.Class ( lift )
addIO :: MaybeT IO Int
addIO = do
lift $ putStrLn "add was called." -- `IO ()`を`MaybeT IO ()`に持ち上げ
a <- MaybeT $ fmap readInt getLine
b <- MaybeT $ fmap readInt getLine
return $ a + b
lift
は (MonadTrans t, Monad m) => m a -> t m a
と定義されており、ここでは IO ()
を MaybeT IO ()
に変換することができています。
これも do 記法を利用せずに書くと、MaybeT
に持ち上げて処理されているのが分かりやすいかと思います。
addIO :: MaybeT IO Int
addIO =
lift (putStrLn "add was called.") >>
MaybeT (fmap readInt getLine) >>= \a ->
MaybeT (fmap readInt getLine) >>= \b ->
return $ a + b
addIO 関数を利用する
さて、このように定義できた addIO
関数ですが、型が MaybeT IO Int
となっており、このままでは IO モナドから利用できません。
main :: IO ()
main = do
result <- addIO -- コンパイルエラー
print result
runMaybeT
関数を利用して、外側にある MaybeT
を剥がして IO a
に戻すことができます。
main :: IO ()
main = do
result <- runMaybeT addIO -- result :: Maybe Int
print result
runMaybeT
は MaybeT m a -> m (Maybe a)
型で定義されており、今回は MaybeT IO Int
から IO (Maybe Int)
に変換したというわけですね。
MaybeT モナドの実装
MaybeT モナドの実装 を確認すると以下のようになっています(簡略化しています)。
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
instance (Monad m) => Monad (MaybeT m) where
return = MaybeT . return . Just
x >>= f = MaybeT $ do
v <- runMaybeT x
case v of
Nothing -> return Nothing
Just y -> runMaybeT (f y)
分かりやすいようにコメントを追加すると以下のようになります(一部の式の型注釈もコメントに追加しています)。
instance (Monad m) => Monad (MaybeT m) where
-- return :: a -> MaybeT m a
return = MaybeT . return . Just
-- (>>=) :: (MaybeT m a) -> (a -> MaybeT m b) -> MaybeT m b
x >>= f = MaybeT $ do -- ④ 最後に`MaybeT`で包む
-- runMaybeT x :: m (Maybe a)
-- v :: Maybe a
v <- runMaybeT x -- ① MaybeT を剥がして、中身の`Maybe a`を取り出し、
case v of -- ② 普段の Maybe モナドと同じように処理し、
Nothing -> return Nothing
-- f y :: MaybeT m b
-- runMaybeT (f y) :: m (Maybe b)
Just y -> runMaybeT (f y) -- ③ `f`を適用した結果を`runMaybeT`で再び剥がし、
このように見てみると、MaybeT
で包んだり、runMaybeT
で剥がしたりする処理が含まれているものの、中身の実装としては Maybe モナドの考え方と変わらないことが分かります。
まとめ
もっとも理解しやすいであろう Maybe モナドに対応するモナド変換子の1つである MaybeT
について、利用するモチベーションや基本的な使い方を見てきました。
- 1つの do 式では1つのモナドしか扱えない。(IO モナドと Maybe モナドを同時には扱えない)
- その制限を突破するの方法の1つがモナド変換子と呼ばれるもの。
- モナド変換子は、別のモナドを包み込んだモナドを作ってしまおうという考え方。
- その1つの例が MaybeT で、IO を包み込んだ
MaybeT IO
といったモナドが作れる。 -
MaybeT
は Maybe モナドと同じような挙動をする。 -
IO (Maybe a)
はMaybeT
で包むことができ、IO a
はlift
で包むことができる。
私もまだ深く理解しているわけではありませんが、モナド変換子の学習の入り口として役立てば幸いです。
P.S.
最近は Swift プログラマのための Haskell 入門 という連載ブログ記事を書いてます。
Discussion