Profunctorを咀嚼する

先週のekmett勉強会でliyanghuさんが紹介して下さったprofunctorがちょっと興味深いので、まだ完全に飲み込めては居ないのですが、簡単に纏めてしまおうと思います。

さて、我々にとってFunctorと言えば「fmap :: (a -> b) -> f a -> f b」というメソッドを持った型クラスFunctorの事ですが、圏論で言う所の関手(Functor)を表現した型は、Functor型クラスの他にも色々あるんだそーです。

例えば、Data.Functor.ContravariantというモジュールのContravariantとか、Data.BifunctorのBifunctorとかもそうですし、今回ご紹介するData.ProfunctorモジュールのProfunctorは、ContravariantとBifunctor双方の特徴を併せ持ったようなFunctorの一つです。

ちゃちゃっと型定義を見てみましょう。

class Profunctor f where
  dimap :: (c -> a) -> (b -> d) -> f a b -> f c d

dimapの特徴は、先述のliyanghuさんの文章にある記述がとても解りやすいのでそのまま引用させて頂きます。

      g   ::   a   <-   c
        h ::     b ->     d
dimap g h :: f a b -> f c d

関数gの型定義の矢印が逆向きになっているので、型の作りが一目で解る素晴らしい表記ですね。


で、この矢印が逆向きになる感覚、Contravariant型クラスの実装例を見るとイメージしやすいので、ちょっと見てみましょう。

class Contravariant f where
    contramap :: (b -> a) -> f a -> f b

正直ブログ主、最初この定義を見た時、(゚Д゚)ハァ?ってなりました。
ネタばらしするとシンプルなんですけどね。

newtype Predicate a = Predicate { getPredicate :: a → Bool }

instance Contravariant Predicate where
  contramap g (Predicate p) = Predicate (p . g)
newtype Op b a = Op (a → b)

instance Contravariant (Op b) where
  contramap g (Op f) = Op (f . g)

このように、型 f a が 「aを取る関数」を持っているような場合、contramapの型を満足させる事ができるというワケです。
fが型aの値そのものを持っていなくてはいけないとゆー、しょーもない固定概念が邪魔をして違和感を感じてしまうのですね。


さて、「逆向きの矢印」をどう満足させるかは解ったので、とりあえずprofunctorのdimap関数の型を満足させるFooを適当に考えて見る事にしましょう。

newtype Foo c a b = Foo (a -> c, b)

instance Profunctor (Foo c) where
  dimap f h (Foo (x, y)) = Foo (x . f, h y)

これでいちおうFoo型をProfunctorの型はなんとなく解りましたが、ぶっちゃけ何が嬉しいのか良く解りません。

もうちょい実用的な例は無いんですかー。
と思ったあなたに朗報、実はむっちゃんこ身近に、Profunctorになる型があったりします。

instance Profunctor (->) where
  dimap ab cd bc = cd . bc . ab

そです。関数はめっちゃくちゃ綺麗にProfunctorになるのです。
その働きは型を見れば一発です。

dimap :: (c -> a) -> (b -> d) -> (->) a b -> (->) c d

つまり

dimap :: (c -> a) -> (b -> d) -> (a -> b) -> (c -> d)  

profunctorのインスタンスとしてdimapに渡された関数 (a -> b) は、2つの関数(c -> a)と(b -> d) を繋ぐノリのような役割を果たします。

さらにprofunctorには、以下の型を持つlmapとrmapという関数を持っています。

-- 一つ目の型変数のみmap
lmap :: (a -> b) -> f b c -> f a c
                      ^        ^

-- 二つ目の型変数のみmap
rmap :: (b -> c) -> f a b -> f a c
                        ^        ^

これは、dimapさえ定義されていれば、以下のようにして実装できます。

lmap f = dimap f id
rmap = dimap id

(->)のlmapとrmapの型は、次のように始域と終域を書きかえるという性質を持ちます。

lmap :: (a -> b) -> (b -> c) -> (a -> c)
                     ^           ^

rmap :: (b -> c) -> (a -> b) -> (a -> c)
                          ^           ^

これは、Arrowの持っている性質と同じですね*1

Prelude Control.Arrow> :t (^>>)
(^>>) :: Arrow a => (b -> c) -> a c d -> a b d
Prelude Control.Arrow> :t (<<^)
(<<^) :: Arrow a => a c d -> (b -> c) -> a b d

うーん、なんとなくProfunctorのパワーが見えてきたような気がします。


はてさて、FunctorやMonadもそうですが、こういう抽象的な型について考える場合、現実的な具体例をいくつか上げていって、「それらが全て多相に扱う事ができる」というような捉え方をする必要があるので、「理解=納得」に繋がりにくいのですよね(´・ω・`)

とゆーワケで、実際にHackageを覗いてみて、どんなものがProfunctorになるのか見てみると・・・うげげ、見たことの無い型がいっぱいでござる。

Profunctor (->)	 
Monad m => Profunctor (Kleisli m)	 
Functor w => Profunctor (Cokleisli w)	 
Profunctor (Tagged *)	 
Profunctor (Forget r)	 
Arrow p => Profunctor (WrappedArrow p)	 
Functor f => Profunctor (DownStar f)	 
Functor f => Profunctor (UpStar f)	 

Kleisliっていうのはクレイスリ圏のアレですよね。案の定モナドと仲が良さそうです。CokleisliはComonad関係ですか?なんかどちらも怪しげな香りがしてきます。
Taggedっていうのは・・・ちゃんと見ないと解らなさそうですね、時間が無いのでまた今度。ForgetはConstantにちょっぴり似てます。

WrappedArrowはどうやら、ArrowをProfunctorにラップするための型のようです。
あと、DownStarとUpStarはそれぞれFunctorをProfunctorにラップするための型ですね。
これらはなんか面白そうな雰囲気を醸し出してるのでちゃんと見てみたいかも。

あとは、前回の記事で紹介した、liyanghuさんの発表資料には、さっきのFoo型よりよっぽどマトモな例として、Limitsという型を作ってProfunctorにする例や、IndexedもProfunctorだよ!みたいな例が掲載されているので、ここで再掲させて頂きます。
https://www.fpcomplete.com/user/liyang/profunctors


「PROFUNCTORS EVERYWHERE」という事なので、もっと直感的にdimapを捉えられれば、、かなり視野が開けるかもしれません。

というわけで今回はこれにて、ノシノシ

*1:ekmett勉強会の時のkhibinoさんの受け売りですが