タイプは私の心を吹き飛ばします:
class Contravariant (f :: * -> *) where
contramap :: (a -> b) -> f b -> f a
それから this を読みましたが、タイトルとは逆に、もう悟りがありませんでした。
誰かが反変ファンクタとは何か、いくつかの例を説明できますか?
まず第一に、@ haoformayorの答えは素晴らしいので、これは完全な答えよりも補遺を検討してください。
Functor(co/contravariant)について私が考える1つの方法は、ダイアグラムの観点からです。以下の定義に反映されます。 (私はcontramap
をcmap
と略記しています)
covariant contravariant
f a ─── fmap φ ───▶ f b g a ◀─── cmap φ ─── g b
▲ ▲ ▲ ▲
│ │ │ │
│ │ │ │
a ────── φ ───────▶ b a ─────── φ ──────▶ b
注:これらの2つの定義の唯一の変更点は、上の矢印です(まあ、名前も違うので、それらを別のものとして参照できます)。
これらについて話すときに常に頭に浮かぶ例は関数です-そしてf
の例はtype F a = forall r. r -> a
です(つまり、最初の引数は任意ですが固定r
)。つまり、共通の入力を持つすべての関数です。いつものように、(共変)Functor
のインスタンスはfmap ψ φ
=ψです。 φ`。
ここで(反変)Functor
はすべて共通の結果を持つ関数です-type G a = forall r. a -> r
ここでは、Contravariant
インスタンスはcmap ψ φ = φ . ψ
になります。
しかし、これは一体何を意味するのか
φ :: a -> b
およびψ :: b -> c
通常したがって、(ψ . φ) x = ψ (φ x)
またはx ↦ y = φ x
およびy ↦ ψ y
は理にかなっています。ここでcmap
のステートメントで省略されているのは、
φ :: a -> b
がψ :: c -> a
したがって、ψ
はφ
の結果を取得できませんが、φ
が使用できるものに引数を変換できます。したがって、x ↦ y = ψ x
およびy ↦ φ y
が唯一の正しい選択です。
これは次の図に反映されていますが、ここでは一般的なソース/ターゲットを持つ関数の例を抽象化しました-共変/反変であるという性質を持つものに数学と/またはhaskellでよく見られるものです。
covariant
f a ─── fmap φ ───▶ f b ─── fmap ψ ───▶ f c
▲ ▲ ▲
│ │ │
│ │ │
a ─────── φ ──────▶ b ─────── ψ ──────▶ c
contravariant
g a ◀─── cmap φ ─── g b ◀─── cmap ψ ─── g c
▲ ▲ ▲
│ │ │
│ │ │
a ─────── φ ──────▶ b ─────── ψ ──────▶ c
数学では通常、何かを関手と呼ぶには法律が必要です。
covariant
a f a
│ ╲ │ ╲
φ │ ╲ ψ.φ ══▷ fmap φ │ ╲ fmap (ψ.φ)
▼ ◀ ▼ ◀
b ──▶ c f b ────▶ f c
ψ fmap ψ
contravariant
a f a
│ ╲ ▲ ▶
φ │ ╲ ψ.φ ══▷ cmap φ │ ╲ cmap (ψ.φ)
▼ ◀ │ ╲
b ──▶ c f b ◀─── f c
ψ cmap ψ
これは言うことと同じです
fmap ψ . fmap φ = fmap (ψ.φ)
一方
cmap φ . cmap ψ = cmap (ψ.φ)
_Functor f
_は、a
が「負の位置」に表示されないという主張と考えることができます。これは、このアイデアの難解な用語です。次のデータ型では、a
が「結果」変数として機能するように見えることに注意してください。
newtype IO a = IO (World -> (World, a))
_newtype Identity a = Identity a
_
newtype List a = List (forall r. r -> (a -> List a -> r) -> r)
これらの各例では、a
は正の位置に表示されます。ある意味で、各タイプのa
は、関数の「結果」を表します。 2番目の例のa
を_() -> a
_と考えるとわかりやすいでしょう。また、3番目の例はdata List a = Nil | Cons a (List a)
と同等であることを覚えておくと役立ちます。 _a -> List -> r
_のようなコールバックでは、a
は負の位置に表示されますが、コールバック自体は負の位置にあるため、負であり、負の乗算は正になります。
関数のパラメーターに署名するためのこのスキームは この素晴らしいブログ投稿で詳しく説明されています です。
ここで、これらの各タイプがFunctor
を許可することに注意してください。間違いありません!ファンクタは、「矢印の順序を保持する」、つまり_f a -> f b
_ではなく_f b -> f a
_であるカテゴリカル共変ファンクタのアイデアをモデル化することを目的としています。 Haskellでは、a
が負の位置に出現しないタイプは常にFunctor
を許可します。これらの型はa
で共変であると言います。
言い換えると、Functor
クラスの名前をCovariant
に変更することができます。彼らは同じ考えです。
このアイデアが「never」という言葉で奇妙に表現されている理由は、a
が正と負の両方の場所に出現する可能性があるためです。この場合、タイプはa
で不変であると言えます。 a
も表示されない場合があります(ファントム型など)。その場合、型はa
の共変および反変の両方であると言います–二変量。
したがって、a
が正の位置に出現しないタイプの場合、a
でタイプが反変であると言います。そのようなすべての型_Foo a
_は_instance Contravariant Foo
_を許可します。以下は、contravariant
パッケージから取得したいくつかの例です。
data Void a
_(a
はファントムです)data Unit a = Unit
_(a
が再びファントムになります)newtype Const constant a = Const constant
_newtype WriteOnlyStateVariable a = WriteOnlyStateVariable (a -> IO ())
newtype Predicate a = Predicate (a -> Bool)
newtype Equivalence a = Equivalence (a -> a -> Bool)
これらの例では、a
は二変量または単に反変量です。 a
は表示されないか、否定的です(これらの不自然な例では、a
は常に矢印の前に表示されるため、これを決定するのは非常に簡単です)。その結果、これらの各タイプは_instance Contravariant
_を許可します。
より直感的な演習では、これらのタイプ(反分散を示す)に目を細めて、次に上記のタイプ(共分散を示す)に目を細めて、a
の意味の違いを直感できるかどうかを確認します。多分それは役に立つかもしれませんし、多分それはまだまだ不道徳な手技です。
これらが実際に役立つのはいつですか?たとえば、Cookieのリストを、チップの種類ごとに分割したいとします。 _chipEquality :: Chip -> Chip -> Bool
_があります。 _Cookie -> Cookie -> Bool
_を取得するには、単に_runEquivalence . contramap cookie2chip . Equivalence $ chipEquality
_を評価します。
かなり冗長です!しかし、newtypeによって引き起こされる冗長性の問題を解決することは、別の質問でなければなりません...
トーク:楽しいファン :このトークの素晴らしさを誇張することはできません
この答えは他の答えほど深くは学術的ではないことは知っていますが、それは単に、あなたが遭遇する反変の一般的な実装に基づいています。
まず、ヒント:contraMap
のf
関数型を、map
の同じメンタルメタファーを使用して読み取らないでください。
あなたはあなたの考えを知っています:
「を含む(またはを生成する)
t
というもの」
..._f t
_のような型を読み取ったとき
さて、あなたはそれをやめる必要があります、この場合。
反変関数は、古典的な関数の「二重」なので、contraMap
に_f a
_が表示される場合、「二重」のメタファーを考える必要があります。
_
f t
_は[〜#〜] consumes [〜#〜] at
これで、contraMap
の型が意味を持ち始めます。
contraMap :: (a -> b) -> f b ...
...すぐに一時停止し、タイプは完全に賢明です:
b
を「生成」する関数。b
を「消費」するもの。最初の引数はb
をクックします。 2番目の引数はb
を使用します。
理にかなっていますよね?
タイプの記述を終了します。
contraMap :: (a -> b) -> f b -> f a
したがって、最終的には、これは "consumer of a
"を生成する必要があります。
さて、最初の引数がa
を入力として受け取る関数であれば、確かにそれを構築できます。
関数_(a -> b)
_は、「a
のコンシューマー」を構築するための適切なビルディングブロックである必要があります。
したがって、contraMap
を使用すると、基本的には次のように新しい「コンシューマ」を作成できます(警告:作成されたシンボルを作成します)。
_(takes a as input / produces b as output) ~~> (consumer of b)
_
contraMap
の最初の引数(つまり、_(a -> b)
_)。f b
_)。contraMap
の最終出力(a
の使用方法を知っているもの、つまり_f a
_)。