著者:井上 健太
はじめに
関数型言語における型クラスであるFunctorやApplicative、Monadについて解説します。
これらは型を引数に1つ取る型を1つ引数に取る型クラスであり、これらを応用することによりエラー処理が楽になる等、活用の幅が広くあります。
そこで本記事ではFunctor等の特徴についてまとめます。
まずは型を引数に取る型について解説します。
型を引数に取る型
型を引数に取る型として、1引数ならリストやMaybe/Option型、多引数なら直和型や直積型があります。多引数の場合はCurry化すれば1引数とみなせるので、本記事では1引数の型を引数に取る型として考えます。
ここで、1引数の型を取る型を1つをmとします。
任意の型Aに対しm Aは型となりますが、多くの場合、元の型Aと何か関係付けられるものになっています。
例えばmがMaybe/Optionや直和などの場合、A -> m Aの(自然な)単射が存在し、m AはAを含んでいる、すなわち
A ⊂ m Aのように考えられます。
このような型Aとm Aの間の関係を利用すると、A型の引数をとる関数fからm A型の引数を取る関数を自然に定義できる場合があります。例えばmがMaybeの場合は入力がJust xだった場合、Just (f x)を、Nothingだった場合はそのままNothingを返す関数などです。
このようなA型を引数に取る関数から自然に定義されるm A型の引数を取る関数を簡単に使用できる仕組みがFunctorやApplicative、Monadです。
それぞれの特徴を見ていきましょう。
Functor
Functorは1引数の型を取る型mに対し、主に任意の型A,Bに対するA -> Bの関数からm A -> m Bの関数を作る型クラスです。
実際にhaskellではFunctor型クラスには以下のようなfmap :: (a -> b) -> a -> bが定義されています。
class functor f where
fmap :: (a -> b) -> f a -> f b
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
実際にf::A -> Bに対し、(<$>) fがm A -> m B型の関数になります。
例えばmがMaybe型の場合は前述の通り、f <$> Just xがJust (f x)、f <$> NothingがNothingを返します。
Functorに必要な公理
型を引数に取る型mに対し、Functor mは以下の公理を満たすものでなければなりません。
-
任意の型
A,B,Cと関数f::A -> B,g::B -> C、x::m Aに対して、g . f <$> x = g <$> (f <$> x) -
任意の型
A、x:m Aに対し、id <$> x = x
1つ目の公理は以下のような可換図式が成り立つことを表しています。
2番目の公理は恒等射に関するものです。
主なFunctor
haskellの実装を見ていきます。
Maybe型
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
Either(直和)型
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
[](リスト)型
instance Functor [] where
fmap = map
(,)(直積)型
instance Functor ((,) a) where
fmap f (x,y) = (x,f y)
Applicative
Applicativeの用途は主に任意の型A,
B,
Cに対し、A -> B -> Cなどの多引数関数からm A -> m B -> m C型等の関数を作るものです。
これはFunctor型クラスだけでは実現できないので、別の型クラスApplicativeが必要になります。
実際の定義を見てみましょう。
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 id
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 f x = (<*>) (fmap f x)
(<*>)とliftA2は相互定義になっていて、どちらかを与えればもう片方が自動的に定義されます。
liftA2は2引数ですが、それ以上の引数の場合も定義できます。
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
liftA3 f a b c = liftA2 f a b <*> c
Applicativeに必要な公理
型を引数に取る型mに対し、Applicative mは以下の公理を満たすものでなければなりません。
- 任意の型
A、v::m Aに対し、pure id <*> v = v - 任意の型
A,B,C,D,f::m (B -> C),g::m (A -> B),x::m Aに対し、pure (.) <*> f <*> g <*> x = f <*> (g <*> x) - 任意の型
A,B,f::A -> B,x::Aに対し、pure f <*> pure x = pure (f x) - 任意の型
A,B,f::m (A -> B),x::Aに対し、f <*> pure x = pure ($ x) <*> f
1つ目の公理ですが、これはApplicativeからFunctorが作れることを表しています。
実際、任意の型A, B,
f:A -> Bとx:m Aに対し、f <$> xをpure f <*> xと定義すればこれはまさにFunctorの公理の1つ目そのものです。
Functorのもう一つの公理はApplicativeの2番目と3番目の公理で実現できます。
2番目の公理は少しわかりづらいですが、liftA3を用いて書き直すと次のような結合則を表していることが分かります。liftA3 (.) f g x = f <*> (g <*> x)
3番目の公理はApplicative則というよりはpureと<$>の以下のような可換図式を表しています。
4番目の公理は変数の入れ替えです。
主なApplicative
Maybe型
instance Applicative Maybe where
pure = Just
Just f <*> m = fmap f m
Nothing <*> _m = Nothing
Either(直和)型
instance Applicative (Either e) where
pure = Right
Left e <*> _ = Left e
Right f <*> r = fmap f r
[](リスト)型
instance Applicative [] where
pure x = [x]
fs <*> xs = [ f x | f <- fs, x <- xs]
ライブラリ上ではこの定義だが、実は以下の定義でもApplicative則が成り立つ。
instance Applicative [] where
pure x = [x]
fs <*> xs = [ f x | x <- xs, f <- fs]
この定義だと一般に<*>の出力が異なるが、後述するMonadを作る時の仕様を満たさなくなる。
(,)直積型
instance Monoid a => Applicative ((,) a) where
pure x = (mempty, x)
(u, f) <*> (v, x) = (u <> v, f x)
型aはモノイドである必要があり、単位元memptyと演算<>を使います。
Monad
Monadは主に任意の型A,
Bに対し、A -> m Bからm A -> m Bを作るためのクラスです。
これはApplicativeやFunctorのみから作ることができないため、以下の型クラスを用います。
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
return :: a -> m a
return = pure
MonadはApplicativeを継承していますが、注意が必要なのは実はMonadからApplicativeを作る方法が複数あることです。
Applicativeの継承に関する注意
Monad mから
例えばMonad mがあったとして、このmから以下の2通りのApplicativeが作れます。
class Monad m => VApplicative m where
vpure :: a -> m a
vpure = pure
(<*.>) :: m (a -> b) -> m a -> m b
f <*.> x = f >>= (<$> x)
class Monad m => HApplicative m where
hpure :: a -> m a
hpure = pure
(<*..>) :: m (a -> b) -> m a -> m b
f <*..> x = x >>= \y -> ($ y) <$> f
さらにこの2つは実行結果が一般に異なります。
instance VApplicative [] where
instance HApplicative [] where
fs :: [Int -> Int]
fs = [\n -> n + 1,\n -> n + 2]
ns :: [Int]
ns = [30,40]
main :: IO ()
main = do
print $ fs <*> ns -- [31,41,32,42]
print $ fs <*.> ns -- [31,41,32,42]
print $ fs <*..> ns -- [31,32,41,42]
この2つですが、haskellでは前者の定義(正確にはf <*> x
$=$
f >>= (\y -> x >>= \z -> return (y z)))を満たすものでなければなりません。
これを踏まえながらMonadの以下の公理を確認していきます。
Monadに必要な公理
型を引数に取る型mに対し、Monad mは以下の公理を満たすものでなければなりません。
- 任意の型
A,B、任意のx::A,f::A -> m Bに対し、return x >>= f$=$f x - 任意の型
A、任意のx::m Aに対し、x >>= return$=$x - 任意の型
A,B,C、任意のx::m A,f::A -> m B,g::B -> m Cに対し、x >>= f >>= g$=$x >>= (\y -> f y >>= g)
1,2つ目はreturnに関する公理で、それぞれ>>=の左右に渡したときの性質です。
3つ目は>>=の結合則に関するものになっています。
主なMonad
Maybe型
instance Monad Maybe where
(Just x) >>= k = k x
Nothing >>= _ = Nothing
Either(直和)型
instance Monad (Either e) where
Left l >>= _ = Left l
Right r >>= k = k r
[]リスト型
instance Monad [] where
xs >>= f = [y | x <- xs, y <- f x]
(,)直積型
instance Monoid a => Monad ((,) a) where
(u,a) >>= k = case k a of (v,b) -> (u <> v,b)
型aはモノイドである必要があり、演算<>を使います。
まとめ
これら3つを簡単にまとめると以下のようになります。
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)
Monadは上2つを継承しているので、Monadであればこれら全てが使えます。
これらを用いて型を返す型mの世界で元の世界の関数が使えるようになります。
終わりに
FunctorやApplicative、Monadについて、その特徴についてまとめました。
