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 bClass Contravariant s where
-- aから目的の型を構成する型をbから目的の型を構成する型に変える
contramap :: (b -> a) -> t a -> t b
Functor f => f a
のa
は(関数が対象なら)返値の型を意味しているContravariant s => s a
のa
は引数の型を意味している。
このように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 + 1main = putStrLn $ toDislpay showSucc 1024
Contravariant
を使って先のShowType Int
をShowType Double
に変換するには以下のようにする(なおContravariant
はcabal install contravariant
でインストールすること):
import Data.Functor.Contravariantinstance 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)
関数合成をより一般化したものと考えればよいのだろうか。
Bifunctor
とProfunctor
型構成子が2引数になった場合に対応。
class Bifunctor p where
bimap :: (a -> b) -> (c -> d) -> p a c -> p b dclass 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
型の値を関数f
でb
型の値に変えてから関数h :: b -> IO ()
に送り込むということで、どうということはない話だった。(これに似たものはモナド変換、Continuationあたりで見たような。。。)