技術
Functor, Applicative, Monadの活用
2023.05.15
※前回の記事「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
などを活用の仕方やその特徴について述べました。