TL;DR
Control.Monad.Coにある、各コモナドw
に対して定義されるモナドCo w
は、State
モナドの一般化と見なせる。
背景
kan-extensionsというパッケージに、CoT
というモナド変換子が定義されています。
(Control.Monad.Co)
CoT
は、“Monads from Comonads”(Comonad
から作ったMonad
)と形容されており、
実際に以下のような定義とMonad
のインスタンスを持っています。
type Co w = CoT w Identity
newtype CoT w m a = CoT
runCoT :: forall r. w (a -> m r) -> m r }
{
instance (Functor w) => Functor (CoT w m)
instance (Comonad w) => Applicative (CoT w m)
instance (Comonad w) => Monad (CoT w m)
instance (Comonad w) => MonadTrans (CoT w)
すなわち、Comonad
であるw
に対して、CoT w m
はMonad
です。
私がCoT
というものを知ったのは最近のことではないのですが、「Comonad
をMonad
に作り変えられるとは、なんて神秘的なんだろう」「このforall r.
は何だろう」「Either e
が例外の作用、State s
が可変状態の作用を表すなら、Co w
は何の作用を表すのだろう」と思いはしたものの、特に深く考えてみようとは思っていませんでした。
ですが、最近すこし思い立って1、一般のCoT
ではなくCo w = CoT w Identity
に限って真面目に検討してみました。すると驚いたことに2、Co w
はある意味でState
モナドの一般化であり、とても身近な作用が不思議な見た目をしているだけだということがわかりました。本記事ではこのことについて解説します。
モナド Co w
の具体例たち
解説の本体に入る前に、まずは定義の準備をします。ここでは、モナド変換子であるCoT
ではなく、ただのCo
について考えます。そのため、CoT
を介さないシンプルな定義を用いることにします。
-- kan-extensionsの定義とは異なる
newtype Co w = Co { runCo :: forall r. w (a -> r) -> r }
Co
のモナドのインスタンスは以下のように定義されます。
instance Functor f => Functor (Co f) where
fmap g (Co c) = Co (c . fmap (. g))
instance Comonad w => Applicative (Co w) where
pure a = Co \war -> extract war a
<*>) = ap
(
instance Comonad w => Monad (Co w) where
>>= k = joinCo $ fmap k ma
ma
joinCo :: Comonad w => Co w (Co w a) -> Co w a
= Co $ runCo mma . extend (flip runCo) joinCo mma
「Co w
がState
モナドの一般化である」と言いましたが、実際にCo w
がState
モナドと同型になることがあります。Storeコモナドの場合を考えると、Co (Store s)
はState s
とモナドとして同型です。
data Store s a = Store (s -> a) s
newtype State s a = State (s -> (s, a))
実際、以下のような計算によって同型が確認できます。3
Co (Store s) a
~ ∀r. Store s (a -> r) -> r
~ ∀r. (s -> a -> r, s) -> r
~ ∀r. (s -> a -> r) -> s -> r
~ ∀r. s -> (s -> a -> r) -> r
~ s -> ∀r. (s -> a -> r) -> r
~ s -> (s, a)
~ State s a
その他のコモナドに対してもCo
によってどんなモナドが得られるかを見ていくと、次のようになります。
Co (Env e) a ~ (e -> a) ~ Reader e a
Env](https://hackage.haskell.org/package/comonad-5.0.8/docs/Control-Comonad-Env.html#g:2)コモナドは、`Co`によって`Reader`モナドになります。 [ Co (Env e) a ~ ∀r. Env e (a -> r) -> r ~ ∀r. (e, a -> r) -> r ~ ∀r. e -> (a -> r) -> r ~ e -> ∀r. (a -> r) -> r ~ e -> a ~ Reader e a
Co (Traced m) a ~ (m, a) ~ Writer m a
Tracedコモナドは、
Co
によってWriter
モナドになります。Co (Traced m) a ~ ∀r. Traced (a -> r) -> r ~ ∀r. (m -> a -> r) -> r ~ ∀r. ((m,a) -> r) -> r ~ (m,a) ~ Writer m a
ところで、これらのState s, Reader e, Writer m
というモナドは、
いずれも以下のStateLike _ _
を特殊化した形になっています。
data StateLike m s a = StateLike m (s -> a)
-- State s ~ StateLike (s -> s) s
-- Reader e ~ StateLike () e
-- Writer m ~ StateLike m ()
次節では、どんなコモナドw
に対しても、Co w
というモナドは、
State
モナドを一般化したStateLike
の一例になっていることを説明します。
Co w
は常にStateLike
を使って表すことができる
どんなFunctor f
についても、Co f
は以下のMove f
を用いてStateLike (Move f) (f ())
と同型になります。
newtype Move f = Move { appMove :: forall r. f r -> r }
これは、Co f a
から同型な型を辿って変形させていくことで得られます。
Co f a
~ ∀r. f (a -> r) -> r
~ ∀r. CoYoneda f (a -> r) -> r
~ ∀r. (∃s. (f s, s -> (a -> r))) -> r
~ ∀r. ∀s. (f s, s -> (a -> r)) -> r
~ ∀r. ∀s. f s -> (s -> a -> r) -> r
~ ∀s. ∀r. f s -> (s -> a -> r) -> r
~ ∀s. f s -> ∀r. (s -> a -> r) -> r
~ ∀s. f s -> (s, a)
~ ((∀s. f s -> s), (∀s. f s -> a))
~ (Move f, (∃s. f s) -> a)
~ (Move f, f () -> a)
~ StateLike (Move f) (f () -> a)
先述したように、Store
コモナドに対してCo (Store s)
はState s
と同型でした。
StateLike
による上記の表現を使っても、State s
と同型になることを確認してみましょう。
Move (Store s)
~ ∀r. Store s r -> r
~ ∀r. (s -> r, s) -> r
~ ∀r. (s -> r) -> (s -> r)
~ s -> s
Store s ()
~ (s -> (), s)
~ ((), s)
~ s
Co (Store s) a
~ StateLike (Move (Store s)) (Store s ()) a
~ StateLike (s -> s) s a
~ (s -> s, s -> a)
~ s -> (s, a)
~ State s a
多くのコモナドw
に対してCo w
モナドを求める場合、StateLike
を用いたほうが簡単になる場合があります。
例えば、長さ1以上のリストNonEmptyはコモナドになっています。
> import Control.Comonad
ghci> import Data.List.NonEmpty
ghci> xs = 4 :| [3,2,1]
ghci> extract xs -- == head xs
ghci4
> duplicate xs -- == tails1 xs
ghci4 :| [3,2,1]) :| [3 :| [2,1],2 :| [1],1 :| []]
(> toList . fmap toList $ duplicate xs
ghci4,3,2,1],[3,2,1],[2,1],[1]] [[
このことによってCo NonEmpty
はモナドになっています。これがどのようなモナドなのかをCo
の定義から考えると中々大変なのですが、StateLike
による表現を用いると以下のようになります。4
NonEmpty ()
~ (NonEmptyリストの長さの集合)
~ (1以上の整数の集合) = N
Move NonEmpty
~ ∀r. NonEmpty r -> r
~ ∀r. (r + (r,r) + (r,r,r) + … ) -> r
~ ∀r. (r -> r) × ((r,r) -> r) × ((r,r,r) -> r) × …
~ (∀r. r -> r) × (∀r. (r,r) -> r) × (∀r. (r,r,r) -> r) × …
~ {1} × {1,2} × {1,2,3} × …
~ { f :: N -> N | ∀n. f n ≤ n }
type Shrinking = { f :: N -> N | ∀n. f n ≤ n }
Co NonEmpty
~ StateLike (Move NonEmpty) (NonEmpty ())
~ StateLike Shrinking N
ここで、Shrinking = { f :: N -> N | ∀n. f n ≤ n }
は、f n
がn
以下になるような関数f :: N -> N
を集めた集合です。
StateLike (N -> N) N
はState N
と同型でした。Shrinking
をN -> N
の部分集合と見て、その包含写像によってStateLike Shrinking N
をStateLike (N -> N) N
に写す自然変換はモナド準同型になります。(ただし注意が必要で、Move NonEmpty
とShrinking
の間の同型をここでは具体的には指定していません。モナド準同型にするには適切な同型を選ぶ必要があります。)
まとめと今後
Co w
という「コモナドから作ったモナド」は、StateLike
というState
モナドを一般化したような形のモナドになることを大雑把ながら説明しました。
実は、Comonad
のうち、「性質のよい」Functor
である多項式Functor
5でもあるものならば、どんなComonad w
も、あるCategoryのインスタンスを使って表現できる、という先行研究があります。6
このCategory
による表現を使うと、よりCo w
がState
らしく思えるCo w
の特徴付けができます。これについても書きたかったのですが、説明に必要な事項が多く私が書くことを断念してしまったので、それはいつの日にかやることにしました。7
https://twitter.com/viercc/status/1647938927372943360↩︎
個人的な感想です↩︎
ほんとうは、単に同型であるだけでなく、具体的に同型を構成して、その同型写像ががモナドとしての同型写像でもあることまで確認しなければいけないのですが、ここでは省略します。↩︎
ただし、無限リストの存在は無視しています↩︎
このブログでは モナドを見分けるコツや多項式Applicative Functorの一般論 などで使いました。↩︎
Daniel Ahman and Tarmo Uustalu “Directed Containers as Categories.” https://arxiv.org/abs/1604.01187↩︎
本当にやる気がでるのか自信がない↩︎