関数型プログラミングでは、関数の適用や合成を繰り返し行うことでプログラムを作成します。
モナドは抽象的な概念なので、関数型プログラミングの学習初期はモナドの有り難みが分かりづらいです。
今回はMonadを使うと、特定の文脈を維持したまま分岐処理が実現できることを確認してみます。
Monad型クラスの定義
まずは、Monad型クラスの定義をさらっと見ておきましょう。
class Applicative m => Monad (m :: * -> *) where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
1行目の記述は以下のようになっています。
class Applicative m => Monad (m :: * -> *) where
上記の定義からMonad型クラスのインスタンスは、Applicative型クラスのインスタンスでもあることが分かります。あとで見るように、モナドは「return」と「(>>=)」という関数を持ち合わせていることから、MonadはApplicativeよりも強力であることが伺えます。
また、型変数mのカインドは「* -> *」ということも分かります。
return :: a -> m a
returnの型定義は、Applicativeにおけるpureと一緒です。
ある値をMonadの文脈に持ち上げます。
Monad型クラスの(>>=)の型定義を見てみましょう。
(>>=) :: Monad m => m a -> (a -> m b) -> m b
(>>=)の第1引数のm aはmをモナドとするモナド値です。
第2引数は、具体的な値をとってm bという新たなモナド値を返す関数です。
最終的にm bの型の値を得ることができます。
モナドは文脈を維持したまま分岐処理ができる
モナドを使えば、特定の文脈を維持したまま、以前に実施した計算の結果に依存した分岐処理が実現できます。
FunctorとApplicativeにより、関数を特定の文脈に適用し使用することができました。
しかし、実はこれだけだと困ったことがあります。
直前の計算結果に依存した分岐処理がFunctorとApplicativeだけではできないのです。
どういうことか、Applicativeを使った例を見てみましょう。
-- User :: String -> Int -> User
data User = User
{ userName :: String
, userAge :: Int
} deriving (Show)
getName :: Maybe String
getName = Just "Taro"
getAge :: Maybe Int
getAge = Just 20
getUserA :: Maybe User
getUserA = User <$> getName <*> getAge
main :: IO ()
main = do
print getUserA -- Just (User {userName = "Taro", userAge = 20})が出力される
getUserA = User <$> getName <*> getAge
{-
上記の記法に馴染みがない方は、以下の型を追って確認してみてください。
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
User :: String -> Int -> User
(User <$>) :: Functor f => f String -> f (Int -> User)
(User <$> getName) :: Maybe (Int -> User)
(User <$> getName <*>) :: Maybe Int -> Maybe User
(User <$> getName <*> getAge) :: Maybe User
-}
上記で、Functorのfmap(<$>)関数とApplicativeの<*>関数を使用していますが、
getName関数とgetAge関数で得られた値を、そのまま適用しているだけであることに注目してください。
一方、Monad型クラスの(>>=)を使えばgetName/getAge関数で得られた値に応じて、処理を分岐することができます。
getUserM =
getName >>= \name ->
getAge >>= \age ->
(if age > 10 then Just (age+10) else Nothing) >>= \newAge ->
return $ User name newAge
main = do
print getUserA -- Just (User {userName = "Taro", userAge = 20})が出力される
print getUserM -- Just (User {userName = "Taro", userAge = 30})が出力される
上記のgetUserM関数では、getAge関数で得られた値により最終的な計算結果を代えることができています。
改めて、getUserAとgetUserMを見比べてみると、Applicativeでは関数から得た値をそのまま適用しているだけで、分岐処理が行えないことが確認できます。
-- ApplicativeスタイルではgetName/getAgeで得られた値をそのまま適用するだけ
-- モナドよりも処理が制限されるので、分岐が不要な場合はこちらの記法が好まれたりする
getUserA :: Maybe User
getUserA = User <$> getName <*> getAge
-- do記法
getUserM :: Maybe User
getUserM = do
name <- getName
age <- getAge
if age > 20 -- 分岐処理ができる
then Just $ User name (age+10)
else Nothing
プログラムの作成において、分岐処理は欠かすことができません。
上記の例だけでも、Monad型クラスが提供する(>>=)がいかに強力か分かると思います。
今回は、Monadの側面の一つである分岐処理の実現について見てみました。