著者:井上 健太
はじめに
関数型言語における型クラスである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
について、その特徴についてまとめました。