web-dev-qa-db-ja.com

反変ファンクタとは何ですか?

タイプは私の心を吹き飛ばします:

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

それから this を読みましたが、タイトルとは逆に、もう悟りがありませんでした。

誰かが反変ファンクタとは何か、いくつかの例を説明できますか?

35
rityzmon

まず第一に、@ haoformayorの答えは素晴らしいので、これは完全な答えよりも補遺を検討してください。

定義

Functor(co/contravariant)について私が考える1つの方法は、ダイアグラムの観点からです。以下の定義に反映されます。 (私はcontramapcmapと略記しています)

      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 (ψ.φ)
15
epsilonhalbe

まず、私たちの友人であるFunctorクラスについてのメモ

_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によって引き起こされる冗長性の問題を解決することは、別の質問でなければなりません...

その他のリソース(見つけたらここにリンクを追加してください)

14
hao

この答えは他の答えほど深くは学術的ではないことは知っていますが、それは単に、あなたが遭遇する反変の一般的な実装に基づいています。

まず、ヒント:contraMapf関数型を、mapの同じメンタルメタファーを使用して読み取らないでください。

あなたはあなたの考えを知っています:

を含む(またはを生成するtというもの」

..._f t_のような型を読み取ったとき

さて、あなたはそれをやめる必要があります、この場合。

反変関数は、古典的な関数の「二重」なので、contraMapに_f a_が表示される場合、「二重」のメタファーを考える必要があります。

_f t_は[〜#〜] consumes [〜#〜] a t

これで、contraMapの型が意味を持ち始めます。

contraMap :: (a -> b) -> f b ...

...すぐに一時停止し、タイプは完全に賢明です:

  1. bを「生成」する関数。
  2. 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)_)。
  • 右側:2番目の引数(つまり、_f b_)。
  • 全体が結びついている:contraMapの最終出力(aの使用方法を知っているもの、つまり_f a_)。
7
Alexander