\gdef\Set{\mathrm{\mathbf{Set}}} \gdef\Hask{\mathrm{\mathbf{Hask}}} \gdef\Vect{\mathrm{\mathbf{Vect}}} \gdef\Mon{\mathrm{\mathbf{Mon}}} \gdef\id{\mathrm{id}} \gdef\Id{\mathrm{Id}} \gdef\map{\mathrm{map}} \gdef\op#1{{#1}^{\mathrm{\scriptsize op}}} \gdef\listof#1{\left\lbrack{#1}\right\rbrack}
前回の続きです。
Functor
以外の関手を使って、次のモナドを分析していこうとしていました。
Cont r
(Control.Monad.Trans.Cont)ListT m
(Control.Monad.Trans.List, “ListT done wrong”)
しかし、Functor
以外の関手などというものをこれまできちんと定義してこなかったツケを返済するため、
前回は圏・関手・自然変換といった(数学の)用語の意味について、
Haskellでの例を引きながら説明しました。
今回はようやく本題に入れます。
Stateモナドの一般化に挑戦する
以前の記事では、
State
モナドを以下の二つのFunctor
の合成と考えることで、
きれいなストリング図が書けることを紹介しました。
-- G s = (s ->)も F s = (s, )もFunctor
type G s = (->) s
type F s = (,) s
newtype State s a = State { runState :: G s (F s a) }
-- State s a ~ Compose (G s) (F s) a
open :: a -> G s (F s a) -- a -> (s -> (s, a))
= \s -> (s, a)
open a
close :: F s (G s a) -> a -- (s, s -> a) -> a
= f s close (s, f)
ここで使った”トリック”は、
恒等関手Identity
からふたつの関手の合成G s ∘ F s
をつくる自然変換
open
と、逆にF s ∘ G s
からIdentity
を取り出すclose
を考えるというものでした。
さて、モナドを合成関手と見て、特別な自然変換からモナド演算pure, join
を導くという手法は、
別の回でもやっています。
「二つのモナドm
のn
に対する分配swap :: ∀a. n (m a) -> m (n a)
を見つけることで、
m∘n
をモナドにする」という方法で、WriterT
やReaderT
を攻略しました。
では、open
,close
を使う手法で作れるモナドはState
モナドのほかに無いのでしょうか?
実は、State
モナドと同じ形のモナドしか作ることができないことが証明できます。
あるF
とG
がFunctor
であり、自然変換open, close
を次のように持っていたとします。
-- 未知のF, GなるFunctor
instance Functor F
instance Functor G
-- 自然変換open, close
open :: a -> G (F a)
close :: F (G a) -> a
newtype M a = M { runM :: G (F a) }
instance Monad M where -- etc.
そして、open, close
が上記ストリング図に書いた関係式を満たしているとします。
Haskellの式で書くなら、以下の等式になります。
>>> fmap close = id :: G a -> G a
open fmap open >>> close = id :: F a -> F a
このとき、任意の型x
について、G x
がF () -> x
と同型であり、
またF x
が(F (), x)
と同型であることが証明できてしまいます。
全部書き下すと長くなってしまうので概略だけ紹介します。
-- runG, makeGが同型の証拠になっている
runG :: G x -> (F () -> x)
= close $ fmap (\() -> gx) f1
runG gx f1 = fmap (\() -> gx) >>> close
runG gx
makeG :: (F () -> x) -> G x
= fmap fn (open ())
makeG fn
-- runG >>> makeG = id の計算だけ示す。
makeG (runG gx)= makeG (fmap (\() -> gx) >>> close)
= fmap (fmap (\() -> gx) >>> close) (open ())
= open >>> fmap (fmap (\() -> gx)) >>> fmap close $ ()
-- openが自然変換 Identity ~> Compose G F であることを使う
-- Identityについては、fmap f = Identity . f . runIdentity
-- Compose G Fについては、fmap f = Compose . fmap (fmap f) . getCompose
-- newtypeを無視すれば、open >>> fmap (fmap f) = f . open
= (\() -> gx) >>> open >>> fmap close $ ()
-- open >>> fmap close = id
= (\() -> gx) $ ()
= gx
結果的に、G (F x)
はF () -> (F (), x)
と同型、
すなわちState (F ()) x
と同型になってしまい、
更にopen, close
から作ったモナド演算pure, join
もState (F ())
のモナド演算と
同型の違いを除いて同じになってしまいます。
つまり、State
モナドとは違うモナドは作れませんでした。
随伴
State
モナドを作る材料となったF
とG
はFunctor
、すなわち\Haskから\Haskへの関手でした。
これを任意の圏の間の関手に一般化すると、随伴とよばれるものになります。
圏Cから圏Dへの関手Fと、 圏Dから圏Cへの関手G(逆向き)について、 FがGの左随伴であるということを次のように定義します。
自然変換\mathrm{unit} \colon \Id_C \to G\circ Fが存在する。
\Id_Cは圏C上の恒等関手です。
自然変換の定義を当てはめれば、すべてのCの対象aに対して定義された射の族 \mathrm{unit}_a \in C(\Id_C(a), G(F(a)))で、任意の射f\in C(a,b)に対して \mathrm{unit}_b \circ \map_{\Id_C}(f) = \map_{G\circ F}(f) \circ \mathrm{unit}_a を満たすもののことです。
恒等関手をわざわざ書かないことにすれば、\mathrm{unit}_a \in C(a, G(F(a)))となります。
open
の型∀a. a -> G s (F s a)
と見比べれば、\mathrm{unit}はopen
の一般化として考えることができます。
自然変換\mathrm{counit} \colon F\circ G \to \Id_Dが存在する。
\Id_Dは圏D上の恒等関手です。
\mathrm{counit}は
close
の一般化として考えることができます。
\mathrm{unit}と\mathrm{counit}は、以下のジグザグ関係式を満たします。
\begin{equation} \mathrm{counit}_{F(a)} \circ \map_F(\mathrm{unit}_a) = \id_{F(a)} \end{equation} \begin{equation} \map_G(\mathrm{counit}_a) \circ \mathrm{unit}_{G(a)} = \id_{G(a)} \end{equation}
随伴の関係にあることは、以下のような色々な言い回しで表現されます。
- FがGの左随伴である
- Gが左随伴Fを持つ
- GがFの右随伴である
- Fが右随伴Gを持つ
- F \dashv G
ここで、C=\Haskである場合を考えます。すなわち、Fは\HaskからDへの関手、
GはDから\Haskへの関手です。このとき、G\circ Fは\Haskから\Haskへの関手で、
State
と同じようにしてMonad
になります。
\begin{equation} \mathrm{pure} = \mathrm{unit} \colon \forall a. a \to G(F(a)) \end{equation} \begin{equation} \mathrm{join} = \map_G(\mathrm{counit}) \colon \forall a. G(F(G(F(a)))) \to G(F(a)) \end{equation}
より一般に、「圏C上のモナド」を考えることができて、Monad
は「\Hask上のモナド」です。
C=\Hask以外の場合でもG\circ FはC上のモナドです。
随伴をストリング図に描く
以前の記事で導入したストリング図は、
Functor
すなわち\Haskから\Hask自身への関手と、それらの間の自然変換だけを考えていました。
ですが、その記事で説明したストリング図は、そのままでも任意の圏どうしの関手を扱うように拡張できます。
例えば、関手Fはストリング図では線で描かれます。 そして、Fが圏Cから圏Dへの関手であったなら、Fの線の左側の領域に「ここは圏Cである」、 右側の領域に「ここは圏Dである」と書き込むことで、この関手の型を明示することができます。
同じように、圏Dから圏Cへの関手Gも描けます。
(ここでは模様付きで領域に色を塗りましたが、別にそこまでする必要はありません。 わかり易さのためにラベル付けしているだけであって、無くてもかまわないものです。)
ストリング図に複数の関手の合成関手が出てきたとき、それは並行する線で表されていました。 合成関手G\circ Fは並行するFとGの線で表されており、 Fの右側の領域はGの左側の領域でもあるのですが、F,Gが合成できているため、 この領域に書き込まれる圏の名前は一致しているはずです。 なので、ひとつの領域には丁度ひとつの圏のラベルが書き込まれます。
FがGの左随伴だとします。このとき、自然変換\mathrm{unit}, \mathrm{counit}は、 次のようにストリング図に描かれます。
ジグザグ関係式も、以下のように描けます。
随伴からCont r
を作る
State s
以外のモナドで、随伴から作られているものの例として、
まずは継続モナドCont r
について考えます。
継続モナドCont r
は、関手(_ -> r)
を二つ合成したものと見ることができます。
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
-- Cont r a ~ ((_ -> r) ∘ (_ -> r)) a
instance Monad (Cont r) where
pure = pure' >>> Cont
= fmap runCont >>> runCont >>> join' >>> Cont
join
pure' :: a -> ((a -> r) -> r)
= \ar -> ar a
pure' a
join' :: ((a -> r) -> r) -> r) -> r) -> ((a -> r) -> r)
join' arrrr= arrrr . pure'
= \ar -> arrrr (pure' ar)
= \ar -> arrrr (\arr -> arr ar)
ここで、(_ -> r)
は、Contravariant
すなわち\op\Haskから\Haskへの関手です。
-- 実際にはこのような記述はできない、疑似コードです
instance Contravariant (_ -> r) where
contramap :: (a -> b) -> (b -> r) -> (a -> r)
= g . f contramap f g
また、一般論として\op{C}からDへの関手は、
Cから\op{D}への関手と見なすこともできます1。そのため、
(_ -> r)
は\Haskから\op\Haskへの関手、かつ\op\Haskから\Haskへの関手です。
ですので、Cont r
は(見た目は同じですが)二つの関手
(_ -> r)
: \Haskから\op\Hask(_ -> r)
: \op\Haskから\Hask
を合成したCont r = (_ -> r) ∘ (_ -> r)
として考えることができます。
先程の一般論「関手Fが関手Gの左随伴なら、G\circ Fはモナドである」と、
Cont r
がモナドであることを踏まえると、
「Cont r
がモナドになるのは(_ -> r)
が(_ -> r)
の左随伴だったからなのでは!?」と予想できるでしょう。
実際のところ、それは正しいです。
type Hask = (->)
newtype Op a b = Op { getOp :: b -> a }
unit :: Hask a ((a -> r) -> r)
= \k -> k a
unit a
counit :: Op ((a -> r) -> r) a
= Op unit counit
ジグザグ関係式も成り立っていますが、証明は省略します。
つまり、State s
とCont r
という、まったく異なるモナドが、
「随伴の関係にある関手の合成」として同じように理解できるのです。
嬉しいですね!
実は、どんなモナドでも、適切な圏をとってくれば「随伴の関係にある関手の合成」 として書けます。(“Eilenberg-Moore圏”や”Kleisli圏”といったキーワードで調べてください。)
ですが、モナドを随伴の関係にある関手たちに分解する方法は一意ではなくて、 「よく知っている関手が使われていて、理解を深められる」ような分解があるかどうかは別です。
リストモナド[]
を随伴に分ける
リストモナド[]
も、随伴\listof{} = U\circ F, F \dashv Uに分けることが可能です。
ここで、Fは\Haskから「モノイドの圏」\Monへの関手、
Uは\Monから\Haskへの関手です。
「モノイドの圏」とは何か、F,Uはどんな関手なのか、 定義を書き下していきます。
「モノイドの圏」\Monを次のように定めます。
\Monの対象は、\Haskの対象である型
a
と、 その型a
のMonoid
のインスタンスを合わせたものです。\Monの射f\in \Mon(a,b)は、関数
f :: a -> b
であって、 モノイド準同型であるものです。- Example:
String
もSum Int
もMonoid
のインスタンスで、\Monの対象です。 - Example:
f = Sum . length :: String -> Sum Int
はモノイド準同型で\Monの射です。 - Non-example:
g = const (Sum 5) :: String -> Sum Int
はモノイド準同型ではないので、\Monの射ではありません。
- Example:
\Haskから\Monへの関手Fを、次のように定めます2。
\Haskの対象a、すなわち型
a
に対して、F(a)はa
のリスト[a]
です。任意の型
a
についてインスタンスMonoid [a]
があるので、[a]
は\Monの対象です。\Haskの射f\in \Hask(a,b)、すなわち関数
f :: a -> b
に対して、 F(f)はmap f :: [a] -> [b]
です。任意のf
についてmap f
はモノイド準同型になっており、 \Monの射の集合\Mon(F(a), F(b))に含まれます。「これは
Functor []
とは何が違うのか?型a
を[a]
に写し、fmap = map
と言っているだけじゃないか!」 と考える人も居るかと思います。何が違うのかというと、[a]
がモノイドであり、map
がモノイド準同型であるという知識を、 「\Haskより限定的な圏\Monへの関手である」と表しているのです。 言うなれば、「実質的には同じだけれど、より”強い”型を付けている」と思ってください。
\Monから\Haskへの関手Uを、次のように定めます3。
\Monの対象aは型なので、\Haskの対象でもあります。 U(a)は、\Monの対象aに対して、 それが
Monoid
のインスタンスを持っていることを忘れて、\Haskの対象と見なします。\Monの射f \in \Mon(a,b)はモノイド準同型
f :: a -> b
でした。 U(f)は、これが準同型だったことを忘れて、単なる関数として\Haskの射と見なします。
そして、FはUの左随伴です。
- 自然変換\mathrm{unit}は、関数
unit = (\a -> [a]) :: a -> [a]
です。 - 自然変換\mathrm{counit}は、モノイド準同型
counit = mconcat :: Monoid a => [a] -> a
です4。
ジグザグ関係式を確かめてみましょう。 \Haskと\Monのどちらの圏なのかは(実際の対象と射は共通で、制限の有無しか違わないので) 雑に無視して計算すれば、以下のようになります。
\begin{equation} \begin{split} U(\mathrm{counit}) \circ \mathrm{unit} &= U(\mathrm{counit}) \circ (\lambda a. \listof{a}) \\ &= \mathrm{mconcat} \circ (\lambda a. \listof{a}) \\ &= \id_{\listof{a}} \end{split} \end{equation}
\begin{equation} \begin{split} \mathrm{counit} \circ F(\mathrm{unit}) &= \mathrm{counit} \circ \map(\mathrm{unit}) \\ &= \mathrm{mconcat} \circ \map (\lambda a. \listof{a}) \\ &= \id_{\listof{a}} \end{split} \end{equation}
この随伴によってU\circ FはMonad
になります。
このMonad
は通常のMonad []
と全く同じものです。
ListT m
は随伴のサンドイッチだった
前節では[]
を随伴F\colon \Hask\to\MonとU\colon\Mon\to\Hask
のペアとして表しましたが、これだけでは有用には見えません。そこで、
応用例を出してみましょう。
関手F\colon C\to Dに右随伴G\colon D\to Cがあれば、
G\circ F\colon C\to Cがモナドになるということを説明しました。
これは、自然変換open, close
を使ったState
モナドの構成の一般化だったのですが、
StateT
モナド変換子もopen, close
を使って作れたことを思い出してください(過去記事)。
StateT
モナド変換子は、関手F s = (s, _)
と関手G s = (s -> _)
の随伴があるとき、任意のモナドm
に対して
G s ∘ m ∘ F s
もモナドになることを原理としていました。
同じことが任意の圏のあいだの随伴でも言えます。上記F,Gに対して、圏D上のモナドTがあれば、
StateT
の場合とまったく同じようにしてG\circ T\circ F\colon C\to Cが圏C上のモナドになります。
これを、リストモナド[]
をつくる随伴F\colon \Hask\to\Mon, U\colon\Mon\to\Haskに適用してみましょう。
つまり、\Mon上のモナドTに対して、G\circ T\circ Fというモナドが作れます。
このモナドは一体どういったものなんでしょうか?
\Mon上のモナドとは何か、というところから考えてみます。一般に圏C上のモナドTとは
- 関手C\to Cであって
- 自然変換\mathrm{pure}\colon \Id_C \to Tを持ち
- 自然変換\mathrm{join}\colon T\circ T\to Tを持ち
- \mathrm{pure}と\mathrm{join}がモナド則(右単位則、左単位則、結合則)を満たす
ものです。つまり、\Mon上のモナドTとは、以下のようなものです。
関手\Mon\to\Monである、つまり
- 任意のモノイド
a
についてT a
もモノイドで - 任意のモノイド準同型
f :: a -> b
についてモノイド準同型map_T f :: T a -> T b
があって map_T id = id
とmap_T f . map_T g = map_T (f . g)
を満たす
- 任意のモノイド
自然変換\mathrm{pure}\colon \Id_{\Mon} \to Tを持つ、つまり
- 任意のモノイド
a
についてモノイド準同型pure :: a -> T a
を持ち、 map_T f . pure = pure . f
を満たす
- 任意のモノイド
自然変換\mathrm{join}\colon T\circ T\to Tを持つ、つまり
- 任意のモノイド
a
についてモノイド準同型join :: T (T a) -> T a
を持ち、 map_T f . join = join . map_T (map_T f)
を満たす
- 任意のモノイド
(pure, join)
がモナド則(右単位則、左単位則、結合則)を満たす
さて、モノイドの圏\Monは\Haskの対象と射を制限したものでした。
そのため、\Hask上のモナドすなわち普通のMonad
から、
\Mon上のモナドにもなっているものを探すのは不自然ではないでしょう。
T
をあるMonad
だとします。任意のモノイドa
に対して、
T a
をモノイドにする方法があります。
instance Semigroup a => Semigroup (T a) where
<> ty =
tx do x <- tx
<- ty
y pure (x <> y)
instance Monoid a => Monoid (T a) where
mempty = pure mempty
T a
のモノイド演算が上記のものであると仮定し、
map_T = fmap
と定義すれば、T
は\Monから\Monへの関手になります。
map_T :: (Monoid a, Monoid b) => (a -> b) -> T a -> T b
= fmap
map_T
-- f :: a -> b がモノイド準同型であると仮定すると、
-- map_T f :: T a -> T b もモノイド準同型になる
mempty :: T a)
map_T f (= map_T f (pure mempty)
= pure (f mempty :: a)
= pure (mempty :: b) -- f はモノイド準同型
= mempty :: T b
<> ty)
map_T f (tx = fmap f $ do
<- tx
x <- ty
y return (x <> y)
= do x <- tx
<- ty
y return (f (x <> y))
= do x <- tx
<- ty
y return (f x <> f y) -- f はモノイド準同型
= do x' <- fmap f tx
<- fmap f ty
y' return (x' <> y')
= map_T f tx <> map_T f ty
さらにpure
とjoin
を(\Hask上の)Monad
から流用すれば、上記の条件のほとんどを満たせます。
すでにわかっている条件を打消し線で消します:
関手\Mon\to\Monである、つまり任意のモノイドa
についてT a
もモノイドで任意のモノイド準同型f :: a -> b
についてモノイド準同型map_T f :: T a -> T b
があってmap_T id = id
とmap_T f . map_T g = map_T (f . g)
を満たす
自然変換\mathrm{pure}\colon \Id_{\Mon} \to Tを持つ、つまり
- 任意のモノイド
a
についてモノイド準同型pure :: a -> T a
を持ち、 map_T f . pure = pure . f
を満たす
- 任意のモノイド
自然変換\mathrm{join}\colon T\circ T\to Tを持つ、つまり
- 任意のモノイド
a
についてモノイド準同型join :: T (T a) -> T a
を持ち、 map_T f . join = join . map_T (map_T f)
を満たす
- 任意のモノイド
(pure, join)
がモナド則(右単位則、左単位則、結合則)を満たす
残っているもの、つまり\Mon上のモナド特有の条件はpure
とjoin
がモノイド準同型になることだけです。
pure
とjoin
がモノイド準同型になるためには、以下の4つの等式が成り立っている必要があります。
-- 1
pure mempty = mempty
-- 2
pure (x <> y) = pure x <> pure y
-- 3
mempty = mempty
join -- 4
<> tty) = join ttx <> join tty
join (ttx where ttx, tty :: T (T a)
これらのうち、4番目の式以外は、任意のモナドT
について成り立っています(証明略)。
しかし、4番目のjoin (ttx <> tty) = join ttx <> join tty
はそうではありません。
例えば、T
としてIO
モナドを選ぶと、この式は成り立ちません。
-- T=IOで4番目の式が成り立たない反例
tty :: IO (IO (Sum Int))
ttx,= return (putStrLn "X" >> return (Sum 1))
ttx = putStrLn "Y" >> return (return (Sum 2))
tty
<> join tty
join ttx = do x <- join ttx
<- join tty
y return (x <> y)
= do x <- (putStrLn "X" >> return (Sum 1))
<- (putStrLn "Y" >> return (Sum 2))
y return (x <> y)
= do putStrLn "X"
putStrLn "Y"
return (Sum 1 <> Sum 2)
<> tty)
join (ttx = join $ do
<- ttx
tx <- tty
ty return $ do
<- tx
x <- ty
y return (x <> y)
= do tx <- ttx
<- tty
ty <- tx
x <- ty
y return (x <> y)
= do tx <- return (putStrLn "X" >> return (Sum 1))
<- (putStrLn "Y" >> return (return (Sum 2)))
ty <- tx
x <- ty
y return (x <> y)
= do putStrLn "Y"
putStrLn "X"
return (Sum 1 <> Sum 2)
join (ttx <> tty)
とjoin ttx <> join tty
では出力の順番が違ってしまいます。
すなわち、join
がモノイド準同型ではありません。
この4番目の等式が成り立つことと、T
が可換なモナドであることは同値な条件になります。
(可換なモナドについては過去記事でも取り上げました。)
まとめると、以下のようになります。
- \Mon上のモナドTがあれば、
Monad
すなわち\Hask上のモナドU\circ T\circ Fができる。 - 可換な
Monad
であるT
は、ここまで述べた方法で\Mon上のモナドにできる - したがって、可換な
Monad
であるT
に対して、新しいMonad
であるU\circ T\circ Fができる。
この新しいMonad
は具体的にどんな形をしているでしょうか?
Fはただ、Functor []
の行き先を\Monに制限したものでした。
Uは\Monの制限を忘れるだけでした。したがって、新しいモナドU\circ T \circ Fは以下のように実装できます。
-- T にあたるパラメータの名前は、モナド変換子の慣習にそってmとしました。
newtype ListT m a = ListT { runListT :: m [a] }
-- ListT m ~ m ∘ []
-- 「m は可換なモナドである」のクラス
class Monad m => CommutativeMonad m
-- メソッドは一つも必要ないけれども、
-- 可換なモナドに限ってこのクラスのインスタンスを
-- 提供しなければならない!
instance (CommutativeMonad m) => Monad (ListT m) where
pure a = ListT (pure [a])
= fmap runListT >>> runListT >>> join' >>> ListT
join
join' :: CommutativeMonad m => m [m [a]] -> m [a]
= join . fmap counit'
join'
counit' :: (CommutativeMonad m, Monoid b) => [m b] -> m b
= foldr (<<>>) (pure mempty)
counit' where mx <<>> my = do x <- mx; y <- my; return (x <> y)
これはそう、ListT (done wrong) に他なりません!
つまり、ListT
は、ある意味では間違って(wrong)はいなかったのです。
(もちろん可換なモナドしか使えないというのは”モナド変換子としては”問題で、
そのためにListT done rightが作られたとも言えます。)
\op{C}の射f\in \op{C}(a,b)=Cの射f\in C(b,a)で、\op{D}についても同様なので、 \op{C}からDへの関手Fはf\in C(b,a)を\map_F(f)\in \op{D}(F(b),F(a))に対応させます。 これはすなわちCから\op{D}への関手です。↩︎
Fはよく自由モノイド関手と呼ばれている関手です↩︎
Uはよく忘却関手と呼ばれている関手です。↩︎
ここは実は、
[a]
が無限リストになり得ることを考慮すると破綻しています。 無限個の積が定義できないモノイドがほとんどなので、これは関数にすらなっていません。[a]
は有限リストの型であると思えば、 結合法則をリストの長さと同じくらいの回数使うとcounit
がモノイド準同型であると示せます。↩︎