Functor, Contravariant, Bifuntor, Profunctor

Covariance, contravariance, and positive and negative positionの読書メモ

Contravariant

Functor(すなわちcovariant)とContravariantの違い:

Class Functor t where  
-- tの中のaをbに変える
fmap :: (a -> b) -> t a -> t b
Class Contravariant s where  
-- aから目的の型を構成する型をbから目的の型を構成する型に変える
contramap :: (b -> a) -> t a -> t b
  • Functor f => f aa は(関数が対象なら)返値の型を意味している
  • Contravariant s => s aa は引数の型を意味している。

このようにContravariantはFuntorと逆(双対?)なものになっている。

上で使った「aから目的の型を構成する型」の意味を以下の例で説明する:

data ShowType a = ShowType  
{
toDislpay :: a -> String
}
-- | Intを引数に取る関数(ではなくてShowTypeの1インスタンス)
showSucc :: ShowType Int
showSucc = ShowType $ \x -> if x == 0 then "one" else show $ x + 1
main = putStrLn $ toDislpay showSucc 1024

Contravariantを使って先のShowType IntShowType Doubleに変換するには以下のようにする(なおContravariantcabal install contravariantでインストールすること):

import Data.Functor.Contravariant
instance Contravariant ShowType where
-- contramap :: (b -> a) -> t a -> t b
contramap g (ShowType f) = ShowType (f . g)
main = putStrLn $ toDislpay (contramap floor showSucc) 38.2

つまり、ある型の入力から内部で使用する(従って外からは指定する必要がない)データ構造を生成する「初期化関数」があるとして、それを別の型の入力に適用させる枠組みに使えると。

意義?

先のプログラムは以下でも書ける。

main = putStrLn $ toDislpay showSucc (floor 38.2)

関数合成をより一般化したものと考えればよいのだろうか。

BifunctorProfunctor

型構成子が2引数になった場合に対応。

class Bifunctor p where  
bimap :: (a -> b) -> (c -> d) -> p a c -> p b d
class Profunctor p where  
dimap :: (b -> a) -> (c -> d) -> p a c -> p b d

Profunctorの第2引数はcontravariantではないので注意。

用語: bivariant, invariant

  • bivariant : covariantかつcontravariant
  • invariant : covariantでもcontravariantでもない

型引数の出現位置による判別

covariantかcontravariantかは、型引数の出現位置による。 
例えば、関数(->) rはFunctorかContravariantか?

Class ? ((->) r) where  
-- 間違い
contramap :: (a -> a') -> (a -> r) -> (a' -> r)
-- 正解:rはfmapの第2引数の値域に出現する
fmap :: (r -> r') -> (a -> r) -> (a -> r')
-- >>> (fmap (++ ":OK") show) 1024
-- 1024:OK

ということで ((->) r) はFunctorであることがわかる。

また先のShowTypeの場合では引数の位置に型引数が出ていた。このように「位置」でその型がcovariantなのかcontravariantなのかは自動的に決定できる。

そこで、定義域に現れる場合は負、値域に現れる場合は正とし、高階関数の場合は再帰的に掛けあわせることで得られる値がその型がcovariantなのかcontravariantなのかを常に表すということだった。

もっと難しい例

理解するのに苦労したコード:

newtype Callback a = Callback  
{
runCallback :: (a -> IO ()) -> IO ()
}
instance Functor Callback where  
fmap :: (a -> b) -> Callback a -> Callback b
fmap f (Callback g) = Callback $ \h -> g (h . f)

g, h, fが何を意味するのか混乱したけど、 fの型をa -> bとすると、

  • {L3の定義より}g(a -> IO ()) -> IO () でなければならない
  • fmapの右辺で作るものは:: Callback bでなければならない
  • {L3の定義より}そのためにCallbackの引数のラムダ式の型は(b -> IO ()) -> IO ())でなければならない
  • その引数なのでh :: b -> IO ()
  • なので関数合成h . fの型はa -> IO ()

となって型検査終了。中で生成されるa型の値を関数fb型の値に変えてから関数h :: b -> IO ()に送り込むということで、どうということはない話だった。(これに似たものはモナド変換、Continuationあたりで見たような。。。)

Like what you read? Give Shuji Narazaki a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.