こもなど!コモナド!Comonad!!

どうも。
清く正しいHaskell戦士ちゅーんさんです。

今日はアレです。

Comonad

やってきます。


えー・・・


「こ」


「コ」


「Co」


Coってなんすか。カレーハウスっすか。丸い響きの音を付けて可愛く見せれば良いと思ってるんですか。
いやいや、騙されませんよ、後ろのmonadから察するにどう考えても圏論用語です。本当に(ry

という感じで、名前からして怪しげな臭いをプンプンと漂わせているComonadを見てみたいと思います。
とりあえず、お約束なので、Hackageに行ってComonadクラスの定義を見てきましょう。

http://hackage.haskell.org/packages/archive/comonad/0.1.1/doc/html/Control-Comonad.html

class Functor w => Comonad w where
  extract :: w a -> a
  duplicate :: w a -> w (w a)
  extend :: (w a -> b) -> w a -> w b

  extend f = fmap f . duplicate
  duplicate = extend id

(=>>) :: Comonad w => w a -> (w a -> b) -> w b
(=>>) = flip extend

例によって抽象的で、なんやねんコレ感半端ないですが、よく見るとどれも何処かでみたような型と似ています。

class Monad m where
    return :: a -> m a
    (>>=) :: m a -> (a -> m b) -> m b

join :: Monad m => m (m a) -> m a
join x = x >>= id

MonadとComonadの関数は以下のように対になっているのです。

Comonad Monad
extract:: w a -> a return :: a -> m a
duplicate :: w a -> w (w a) join :: m (m a) -> m a
(=>>) :: w a -> (w a -> b) -> w b (>>=) :: m a -> (a -> m b) -> m b

あ、はい。
だから名前に「monad」が付いてるんですね。

任意のデータ構造をComonadにするためには、extendかduplicateのどちらかを実装すれば良いのですが、これはMonadの(>>=)とjoinがそれぞれお互いを使って次のように定義する事ができるのと対応してます。*1

*Main> :t \f x -> join (fmap f x)
\f x -> join (fmap f x)
  :: (Monad m, Functor m) => (a1 -> m a) -> m a1 -> m a
*Main> :t (>>=id)
(>>=id) :: Monad m => m (m b) -> m b


二値のタプルはFunctorです。

class Functor f where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  	-- Defined in `GHC.Base'

...

instance Functor ((,) a) -- Defined in `GHC.Base'

そして、二値のタプルはComonadでもあります。

class Functor w => Comonad w where
  extract :: w a -> a
  duplicate :: w a -> w (w a)
  extend :: (w a -> b) -> w a -> w b
  	-- Defined in `Control.Comonad'

...

instance Comonad ((,) e) -- Defined in `Control.Comonad'

さて、Monadのjoinは、m (m a) から m aの の自然な変換です。それに対してComonadのduplicateは w a から w (w a) への「自然」な変換になります。
二値のタプルの場合は、一番目の要素を繰り返す事で、(a, b)から(a, (a, b)) へと自然に変換を行うワケです。

*Main> duplicate (1, 10)
(1,(1,10))
*Main> duplicate ("hoge", 10)
("hoge",("hoge",10))

extendは特殊な関手ですね。第一引数で要求している型はextract関数と組み合わせで作ることができます。

*Main> :t (*2) . extract
(*2) . extract :: (Num c, Comonad w) => w c -> c
*Main> :t show . extract
show . extract :: (Show b, Comonad w) => w b -> String

これでfmap同様、第一引数をそのまま、第二引数の値に関数を適用する事ができます。

*Main> extend ((*2).extract) (1, 15)
(1,30)
*Main> extend (show.extract) (1, 100)
(1,"100")

このextendを使ったfmapの実装は、liftWとしてComonadモジュールに定義されています。

liftW :: Comonad w => (a -> b) -> w a -> w b
liftW f = extend (f . extract)

先ほどの型定義でも見たとおり、extend flipした演算子…Comonadの(=>>)はMonadの(>>=)と対になっているのでした。
いつも>>=でやっているようにして、計算を合成していってみましょう。

*Main> ("hoge", 1) =>> (+2) . extract =>> (*2) . extract =>> show . extract
("hoge","6")
*Main> ("hoge", 1) =>> (+2) . extract =>> fst
("hoge","hoge")

で、これちょっとStateっぽいなーと思ったんですが*2、(=>>)の型の性質上、ぶら下がりラムダを使って途中計算で変数を束縛とかできないので上手いこといかず。
大抵の場合はfmapと関数結合(ないしArrow)で事足りそうなイメージ。

これだけ見ると、Monadの(>>=)ほど面白い事はできなさそうですね・・・
結局色々考えてみたのですが、Comonadで一番重要なのは、その本質である「値が取り出せるFunctor」を多相に扱う事ができるという点にあるように見えます。*3
タプルのextractはsndと等価ですね。

*Main> snd (1, 10)
10
*Main> extract (1, 10)
10


もう流石に慣れてきましたが、こういう圏論由来の型に対して即実用性を見つけようとするのはナンセンスなので、使用例とか色々見ながら、感覚掴んでいくのが良さそうです。
そういえば、以前fumievalさんが「Comonadでオブジェクト指向を実現できる」と言っていたような気がしますが、残念ながらソースが見つけられず。



「ところで「Co」って何ですか?」
については、Twitterで @myuon_myon さんに回答頂いたので、内容そのまま紹介させて頂きます。

ブログにコメントするの怖いのでこちらで。Coは「双対」を表す概念で、簡単に言えばある概念Aを可換図式で表した時に、それの射の向きを全てひっくり返したものが「Aの双対」と呼ばれます。ComonadはMonadの双対なのでこの名前がついています。

とゆーことだそうです、ありがとうございましたm(__)m

*1:型クラスに制約はありませんが全てのMonadはFunctorになります

*2:fstがgetとして使えるので

*3:内包された値を自然な形で取り出す事ができるあらゆるFunctorはComonadになると考えて良いのでしょうか?ListやMaybeがComonadにならないのは明らかですね。