お問い合わせ

技術

Functor, Applicative, Monadの活用

2023.05.15

※前回の記事「FunctorとApplicative、Monadを理解する」でそれぞれの特徴についてまとめましたが、本記事ではこれらの活用方法について具体例とともに見ていきます。 Monadなどを使うことによってエラー処理が容易になり、仕様変更などにも強いプログラムにすることが可能になります。



著者:井上 健太

はじめに

前回、FunctorApplicative, 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

と記述してやれば上と同じ処理を行うことができます。

同様にApplicativeMonadの演算も以下のように使用できます。

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型の変数を返せるとは限りません。そこで、そのエラーを処理をするためのあるMonadmに包みm (Key val)を返しています。(イメージとしてはmMaybeEitherだと思えばよいです) また、getKey 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つ目の公理から元のプログラムと一致することが分かります。


まとめ

FunctorApplicativeMonadなどを活用の仕方やその特徴について述べました。


一覧に戻る


採用情報

Recruit


“技術で世界に勝負をかけて未来を拓く”
当社はこのような志で設立され、業界のリーディングカンパニーを目指して努力を続けています。
若い会社であるため、入社した社員全員で歴史を作り上げていくことを実感できることでしょう。
現在、採用活動を積極的に展開中です。
技術で世界に勝負をかける日本インサイトテクノロジーで、存分にお力を発揮してください。


新卒採用情報はこちら
キャリア採用情報はこちら


お問い合わせ

Contact


お問い合わせはこちら