#はじめに
Haskellなどの関数型言語におけるMonadクラスの定義において、Applicativeクラスを継承する場合があります。しかしながら、その定義においてApplicativeの演算子$<*>$が直接使われることがなく、Applicativeクラスを継承することに対する有益性が少ないように思えます。
そこで本記事では、Monadクラスが本当にApplicativeクラスを継承する必要があるのか、MonadクラスとApplicativeクラスとの依存関係から考察していきます。
(数式のインライン表示で勝手に改行され、読みづらい点がありますが、ご了承願います。)
#MonadとApplicativeの定義
まず、MonadとApplicativeの公理系について見てみます。
##Monad
型変換子$m$がMonadであるとは、以下のMonad則と呼ばれる公理を満たす
$\operatorname{unit} : \forall A, A \to m,A$と
$\triangleright : \forall A,B, m,A \to (A \to m,B) \to m,B$が存在することである。
\forall A, \forall p : m\,A, p \triangleright \operatorname{unit} = p
\forall A\,B, \forall e : A,\forall p : A \to m\,B, \operatorname{unit} e \triangleright p = p\,e
\forall A\,B\,C, \forall p : m\,A, \forall q : A \to m\,B, \forall r : B \to m\,C,\\
(p \triangleright q) \triangleright r = p \triangleright (\lambda x. r\,x \triangleright r)
###同値な公理系
型変換子mがMonadであることはmがFunctor1かつ以下を満たす
$\eta : \forall A, A \to m,A$と
$\mu : \forall A, m,(m,A) \to m,A$が存在することと同値である。
\forall A\,B, \forall f : A \to B, \operatorname{fmap} f \circ \eta_A = \eta_B \circ f
\forall A\,B, \forall f : A \to B, \operatorname{fmap} f \circ \mu_A = \mu_B \circ \operatorname{fmap}\,(\operatorname{fmap} f)
\forall A, \mu_A \circ \eta_{(m\,A)} = \operatorname{id}_{(m\,A)}
\forall A, \mu_A \circ \operatorname{fmap} \eta_A = \operatorname{id}_{(m\,A)}
\forall A, \mu_A \circ \operatorname{fmap} \mu_A = \mu_A \circ \mu_{(m\,A)}
実際に、$m$がMonadのとき、
$\eta := \operatorname{unit}$、
$\mu ,x := x \triangleright \operatorname{id}$
と定義すれば、これらの規則を満たすことが分かり、
この公理系に対し、
$\operatorname{unit} := \eta$
$x \triangleright f := \mu,(\operatorname{fmap} f,x)$
と定義すると、Monadであることが分かります。
そこで本記事では、後者の定義である$\eta$と$\mu$を用いた記法でMonadを表現することにします。
##Applicative
型変換子$m$がApplicativeであるとは、以下のApplicative則と呼ばれる公理を満たす
$\eta : \forall A, A \to m,A$と
$<*> : \forall A,B, m,(A \to B) \to m,A \to m,B$が存在することである。
\forall A, \forall x : m\,A, (\eta_{(A \to A)} \operatorname{id}_A) <*> x = x
\forall A\,B\,C, \forall f : m\,(B \to C), \forall g : m\,(A \to B), \forall x : m\,A,\\
(((\eta_{((B \to C) \to (A \to B) \to A \to C)} (\lambda fg. f \circ g)) <*> f) <*> g) <*> x = f <*> (g <*> x)
\forall A\,B, \forall f : A \to B, \forall x : A, (\eta_{(A \to B)}\,f) <*> (\eta_A \,x) = \eta_B\,(f\,x)
\forall A\,B, \forall f:m\,(A \to B), \forall x : A,
f <*> (\eta_A\,x) = (\eta_{((A \to B) \to B)} (\lambda f.f\,x)) <*> f
2番目の公理の意味ですが、
$\operatorname{fmap3} : \forall A,B,C,D, (A \to B \to C \to D) \to m,A \to m,B \to m,C \to m,D$を
\operatorname{fmap3} f\,x\,y\,z := ((\mu_{(A \to B \to C \to D)} \,f) <*> x) <*> y) <*> z
と定義して書き直すと、ある種の結合則を表しているのが分かります。
\forall A\,B\,C, \forall f : m\,(B \to C), \forall g : m\,(A \to B), \forall x : m\,A,\\
\operatorname{fmap3}\,(\lambda fg. f \circ g)\,f\, g\,x = f <*> (g <*> x)
###ApplicativeのFunctor性
ApplicativeからFunctorを定義できます。
具体的には、Applicativeな型変換子$m$に対し、$\operatorname{fmap} : \forall A,B, (A \to B) \to m,A \to m,B$を
\operatorname{fmap} f\,x := (\eta\,f) <*> x
と定義してやればFunctor則を満たすことがわかります。
#MonadとApplicativeの関係
型変換子$m$がMonadであるとき、以下の2通りの方法でApplicativeを定義できます。
##Monadの2つのApplicative性
Monadである型変換子$m$に対し、
<*>^h, <*>^v : \forall A\,B, m\,(A \to B) \to m\,A \to m\,B
をそれぞれ以下に定義する。
f <*>^h x := \mu\,(\operatorname{fmap} (\lambda x. \operatorname{fmap} (\lambda f.f\,x)\,f)\,x)
f <*>^v x := \mu\,(\operatorname{fmap} (\lambda f.\operatorname{fmap} f\,x)\,f)
このとき、
(\eta,<*>^h)\\
(\eta,<*>^v)
はそれぞれApplicative則を満たす。
そこで本記事では便宜上、
$(\eta,<*>^h)$で定義されたものをh-Applicative、
$(\eta,<*>^v)$で定義されたものをv-Applicativeと呼ぶことにします。
h-Applicativeとv-Applicativeは、外側の$\operatorname{fmap}$に渡す引数の順番が異なります。
h-Applicativeでは、$\operatorname{fmap}$の第一引数内で$f$、第二引数で$x$を渡していますが、v-Applicativeではその逆です。
この違いによって、h-Applicativeとv-Applicativeはどちらも同じ型変換子の上のApplicativeではありますが、それぞれの$<*>$演算子は関数として異なる挙動をとります。
例えば、Listモナドの場合、
[\lambda n.n+1, \lambda n.n+2] <*>^h [10,20] = [11, 12, 21, 22]\\
[\lambda n.n+1, \lambda n.n+2] <*>^v [10,20] = [11, 21, 12, 22]
となり、一般には等号が成り立ちません。
すなわち、一般のMonadに対して、異なる2通りのApplicativeを実装することができます。2
#Monadクラスの定義において、Applicativeは継承するべきか?
上記のように、確かにMonadからApplicativeが定義できるので、Monadクラスを定義するときにApplicativeを継承してもいいように思えます。
実際にHaskellではApplicativeを継承してMonadが定義されています。
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
return :: a -> m a
return = pure
しかしながら、Applicativeを継承するということは、Applicativeクラスを1つ固定し、その上にMonadを定義するということになります。
すなわち、Monadに対するApplicativeとして、h-Applicativeかv-Applicativeのどちらか、もしくはそれ以外に存在するかもしれないApplicativeクラスのうち1つが暗黙のうちに固定されているため、もう一つの実装のApplicative演算を使いたい場合に困ったり、これらの混同によるバグ発生の可能性が考えられます。
ならば、Monadクラスの定義自体はApplicativeの演算$<*>$に依存しないことから、Monadクラスの定義ではApplicativeを継承せず、Monad演算とApplicative演算を同時に使う関数だけその両方を仮定し、使い分けができるようにする方がいいのかもしれません。
そもそもの問題として、1つの型変換子に対して複数の異なる定義が存在する可能性があるApplicativeクラスをinstanceとして1つに固定してしまうことに原因があるようにも思えます。
ちなみにHaskellではこの問題に対し、以下の関数を定義することで一種の解決をしていると考えられます。
ap :: Monad m => m (a -> b) -> m a -> m b
ap m1 m2 = do {x1 <- m1; x2 <- m2; return (x1 x2)}
-- Since many Applicative instances define (<*>) = ap, we
-- cannot define ap = (<*>)
Monad
上の関数ap
の実装は本記事におけるv-Applicativeに一致するもので、v-Applicativeを使いたい場合はこちらの定義を用いることができます。
よって、HaskellにおいてはMonadを定義する上で継承するApplicativeクラスはh-Applicativeと等価となるようなものにすれば、これらを使い分けることができます。
Haskellではv-Applicativeと等しいApplicativeを継承する決まりになっているようです。
継承するApplicativeの実装を統一するという方法ならば、継承するクラスの実装が複数ある場合にも混同せずに使用できます。
コメントでのご指摘ありがとうございます。
#まとめ
関数型言語におけるMonadとApplicativeの依存関係について考察し、MonadからApplicative演算への定義は一般には異なる2通り存在することが分かりました。
この結果から、MonadでApplicativeの演算を使う場合は、2通りあるうちのどちらであるかを把握することが重要だと気づきました。
-
Functorの公理は「関数型言語におけるTraversableとは何か?Functorと比較しながら解説する」に記載しています。 ↩
-
IdモナドやMaybeモナドなど、個別のモナドに対してはh-Applicativeとv-Applicativeが一致する場合もあります。 ↩