※前回の記事「FunctorとApplicative、Monadを理解する」でそれぞれの特徴についてまとめましたが、本記事ではこれらの活用方法について具体例とともに見ていきます。 Monadなどを使うことによってエラー処理が容易になり、仕様変更などにも強いプログラムにすることが可能になります。
著者:井上 健太
はじめに
前回、FunctorやApplicative,
Monadの特徴について見てきました。
それをまとめたものが以下になります。
Functor m:A -> Bからm A -> m Bを作る。Applicative m:A -> B -> Cからm A -> m B -> m Cを作る。Monad m:A -> m Bからm A -> m Bを作る。 (それぞれ任意の型A,B,Cに対して)
本記事ではこれらの活用を見ていきます。
エラー処理が楽になる
まず、以下のようなエラーになる可能性のある関数を考えます。
mdiv :: Integral a => a -> a -> Maybe a
mdiv x y =
if y == 0
then Nothing
else Just $ x `div` y
mdivはただの商演算ですが返り値がMaybe型であり、0では割ったときはNothingを返すものになっています。
main = do
print $ 10 `mdiv` 2 -- Just 5
print $ 10 `mdiv` 0 -- Nothing
このエラーかもしれない値を処理しようと思ったとき(例えばこの5倍の値を求めたいとき)、そのまま処理できないのでエラーになっているかどうかで場合分けする必要があります。
-- print $ 5 * (10 `mdiv` 2) -- error
print $ case (10 `mdiv` 2) of
Nothing -> Nothing
Just x -> Just (5 * x)
しかし、これだと処理が増えた時に面倒になり、さらに可読性も悪くなります。
そこで、Functorの演算子<$>を使って書き換えてみます。
今、Functor Maybeにおいて、演算子<$>は(a -> b) -> Maybe a -> Maybe b型を持っているので
-- print $ 5 * (10 `mdiv` 2) -- error
print $ (5 *) <$> (10 `mdiv` 2) -- Just 25
と記述してやれば上と同じ処理を行うことができます。
同様にApplicativeやMonadの演算も以下のように使用できます。
import GHC.Base(liftA2)
main = do
-- print $ (10 `mdiv` 2) * (10 `mdiv` 5) -- error
print $ (*) <$> (10 `mdiv` 2) <*> (10 `mdiv` 5) -- Just 10
print $ liftA2 (*) (10 `mdiv` 2) (10 `mdiv` 5) -- Just 10
-- print $ 10 `mdiv` 2 `mdiv` 5 -- error
print $ (10 `mdiv` 2) >>= (`mdiv` 5) -- Just 1
-- (<*>) :: Applcative m => m (a -> b) -> m a -> m b
-- liftA2 :: Applicative m => (a -> b -> c) -> m a -> m b -> m c
-- (>>=) :: Monad m => m a -> (a -> m b) -> m b
仕様変更が楽になる
先ほどのmdivは再掲するようにMaybe型を返す関数でした。
mdiv :: Integral a => a -> a -> Maybe a
mdiv x y =
if y == 0
then Nothing
else Just $ x `div` y
しかし他にもエラーが出る可能性のある処理があったとすると、Maybe型のままではエラー発生時にはNothingしか返ってこず、どこでエラーが発生したのか分からなくなります。そこで、エラー文を表示するためにMaybe型ではなく以下のようにEither String型に仕様変更したとします。
mdiv :: Integral a => a -> a -> Either String a
mdiv x y =
if y == 0
then Left "Error: divided by zero"
else Right $ x `div` y
従来ならこの変更によりソースの大幅な書き換えが必要になりますが、先ほどのプログラムのようにMonad等の演算のみを使った処理なら変更なしでそのままプログラムが通ります。
main = do
print $ (5 *) <$> (10 `mdiv` 2) -- Right 25
print $ (*) <$> (10 `mdiv` 2) <*> (10 `mdiv` 5) -- Right 10
print $ (10 `mdiv` 2) >>= (`mdiv` 5) -- Right 1
このようにMonadの形で書いておくことで、後で仕様変更があったとしてもMonadとしての処理が同じであれば大幅な書き換えを防ぐことができます。
応用例
データベース処理をするプログラムを考えます。
まずはデータベース関連のライブラリをimportしておきます。
import Control.Monad.Reader(ReaderT,liftIO)
-- データベース処理をするライブラリ
import Database.Persistent
import Database.Persistent.Sqlite
import Database.Persistent.TH
ここで、あるデータベースに保存できるMyData型のデータdata1::MyDataがあったとします。このdata1をそのデータベースに登録し、今登録したデータをそのまま取得し表示する関数dbOperation::DBIO ()を考えます。(DBIOはあるMonadです)
外部データベースに接続するため、この関数の実装にはエラー処理が不可欠ですが、実際にはMonad演算>>=を用いて以下のように簡単に定義できます。
type DBIO a = ReaderT SqlBackend IO a
dbOperation :: DBIO ()
dbOperation =
insert data1 >>= get >>= liftIO . print
ここで、このdbOperationに使われた関数の型を見ていきます。
insert :: (ommited) => val -> m (Key val)
get :: (ommited) => Key val -> m (Maybe val)
print :: Show a => a -> IO ()
class Monad m => MonadIO m where
liftIO :: IO a -> m a
-- (ommited)はmとvalの制約ですが割愛
-- 今回のケースでは、m = ReaderT SqlBackend IO、val = MyDataになります。
insertがある制約を満たした任意のデータの型val型の変数をデータベースに登録し、m (Key val)型の値を返します。Key val型はval型データのデータベースにおけるIDを表す型ですが、外部データベースにアクセスする関係上エラーが起こる可能性があり、必ずKey val型の変数を返せるとは限りません。そこで、そのエラーを処理をするためのあるMonadのmに包みm (Key val)を返しています。(イメージとしてはmがMaybeやEitherだと思えばよいです)
また、getはKey val型のID値をもらい、そのID値のデータベースを検索しその結果をMaybe valで返す関数ですが、上と同様の理由で返り値がmに包まれています。
最後のデータを出力するprintですが、型を合わせるためにliftIOしています。
これでMonad mにおける演算>>=が使えるようになり、insert data1 >>= get >>= liftIO . printの型検査が通ります。
このように、実際には外部データベース接続に関するエラー処理を含むような関数であっても、Monadを用いて簡単に、しかもエラー処理の仕様に依存しない形で記述することが可能になります。
ちなみに実際のエラー処理ですが、Monad Maybeを定義した時のNothingの扱いのように、Monad mの定義によって内部的に行われます。
補足
上のinsert data1 >>= get >>= liftIO . printは以下の
dbOperation :: DBIO ()
dbOperation = do
data1Id <- insert data1
dbdata1 <- get data1Id
liftIO $ print dbdata1
のように手続き型言語風に記述することも可能で、これは
deOperation :: DBIO ()
deOperation =
insert data1 >>= (\data1Id -> get data1Id >>= (\dbdata1 -> liftIO (print dbdata1)))
の略記法になります。これはMonadの3つ目の公理から元のプログラムと一致することが分かります。
まとめ
FunctorやApplicative、Monadなどを活用の仕方やその特徴について述べました。
