这种类型让我大吃一惊:
class Contravariant (f :: * -> *) where
contramap :: (a -> b) -> f b -> f a
然后我读了this,但与标题相反,我没有更开明。
有人可以解释一下逆变函子的含义和一些例子吗?
答案 0 :(得分:33)
从程序员的角度来看,函子的本质是能够轻松地适应事物。我的意思是"适应"这是因为如果我有一个f a
并且我需要一个f b
,我会想要一个适合我f a
形f b
形孔的适配器。
如果我可以将a
转换为b
,我可以将f a
变为f b
,这似乎很直观。确实,这就是Haskell Functor
类所体现的模式;如果我提供了a -> b
函数,那么fmap
可让我将f a
内容改编为f b
个内容,而不必担心f
涉及的内容。 1
当然在这里讨论像list-of-x [x]
,Maybe y
或IO z
这样的参数化类型,以及我们用适配器改变的东西是{{1其中包含{},x
或y
。如果我们希望能够灵活地从任何可能的函数z
获取适配器,那么我们正在适应的事情必须同样适用于任何可能的类型。
不那么直观(起初)是有些类型可以几乎完全与功能相同的方式进行调整,只有它们反向"向后&#34 ;;对于这些,如果我们想要调整a -> b
以满足f a
的需求,我们实际上需要提供f b
函数,而不是b -> a
函数!
我最喜欢的具体例子实际上是函数类型a -> b
(一个用于参数,r用于结果);所有这些抽象的废话在应用于函数时都非常有意义(如果你已经完成了任何实质性的编程,你几乎肯定会在不知道术语或它们有多广泛适用的情况下使用这些概念),并且在这种情况下,这些概念显然是双重的。
众所周知,a -> r
是a -> r
中的一个仿函数。这是有道理的;如果我有一个r
并且我需要a -> r
,那么我可以使用a -> s
函数来简单地通过对结果进行后处理来调整我的原始函数。 2
另一方面,如果我有一个r -> s
函数,我需要的是a -> r
,那么我再次明确表示我可以通过预处理参数满足我的需求在将它们传递给原始函数之前。但我该如何预处理呢?原来的功能是一个黑盒子;无论我做什么,总是期待b -> r
输入。因此,我需要将a
值转换为预期的b
值:我的预处理适配器需要a
函数。
我们刚刚看到的是b -> a
中的函数类型a -> r
是协变仿函数,以及逆变仿函数在r
。我认为这可以说我们可以调整函数的结果,结果类型"随着"而变化。适配器a
,当我们调整函数的参数时,参数类型会改变"反方向"到适配器。
有趣的是,函数结果r -> s
和函数参数fmap
的实现几乎完全相同:只是函数组合(contramap
运算符)!唯一的区别在于您组成适配器功能的哪一方: 3
.
我认为每个街区的第二个定义最具洞察力; (共同)映射到函数的结果是左边的组合(如果我们想要采用"这种情况发生在"视图之后的组合后),同时反复映射函数& #39; s参数是右侧的组合(组合前)。
这种直觉很好地说明了;如果fmap :: (r -> s) -> (a -> r) -> (a -> s)
fmap adaptor f = adaptor . f
fmap adaptor = (adaptor .)
fmap = (.)
contramap' :: (b -> a) -> (a -> r) -> (b -> r)
contramap' adaptor f = f . adaptor
contramap' adaptor = (. adaptor)
contramap' = flip (.)
结构可以为我们提供类型为f x
的值(就像x
函数提供a -> r
值,至少可能,{它可能是r
中的协变Functor
,我们可以使用x
函数将其调整为x -> y
。但是,如果f y
结构从我们这里收到<{1}}类型的值(再次,就像f x
函数类型x
的参数一样),那么它可能是a -> r
仿函数,我们需要使用a
函数将其调整为Contravariant
。
我觉得有趣的是反映出这些来源是协变的,目的地是逆变的&#34;当你从源/目的地的实现者而不是调用者的角度思考时,直觉就会逆转。如果我尝试实施接收y -> x
值的f y
,我可以&#34;调整我自己的界面&#34;所以我开始使用f x
值(同时仍然使用x
函数显示&#34;接收y
值&#34;与我的呼叫者的接口)。通常我们不会这么想;即使作为x
的实施者,我也考虑调整我调用的内容,而不是让调用者的界面适应我&#34;。但这是你可以采取的另一种观点。
我唯一使用x -> y
构建的半现实世界(而不是通过使用右侧合成来隐式使用参数中的函数的逆变,这是非常常见的)用于可以序列化f x
值的Contravariant
类型。 Serialiser a
必须是x
而不是Serialiser
;鉴于我可以序列化Foos,如果可以Contravariant
,我也可以序列化条形码。 4 但是当你意识到Functor
基本上是Bar -> Foo
时,它变得明显;我只是重复Serialiser a
示例的特殊情况。
在纯粹的函数式编程中,没有太多用于获得&#34;接收值的东西&#34;没有它也会回馈所有逆变函子往往看起来像函数,但几乎任何可以包含任意类型值的直接数据结构将是该类型参数中的协变函子。这就是为什么a -> ByteString
提前偷走了这个好名字并在整个地方使用的原因(嗯,那个a -> r
被认为是Functor
的基本部分,已经广泛使用在Functor
被定义为Haskell中的类之前。
在命令式OO中,我认为逆变函子可能更为常见(但不是用Monad
这样的统一框架进行抽象,尽管它也很容易产生可变性和副作用意味着参数化类型根本不可能是一个仿函数(通常:Functor
的标准容器既可读又可写,是Contravariant
的发射器和接收器,而不是意味着&{ #39; s covariant和contravariant,结果表明它不是。)
1 每个人a
的{{1}}个实例说明如何将任意函数应用于a
的特定形式,而不必担心特定类型Functor
正在应用于;关注点很好。
2 这个仿函数也是一个monad,相当于f
monad。我不会在这里详细讨论仿函数,但鉴于我的帖子的其余部分,一个明显的问题是&#34; f
类型也是{{1}中的某种逆变monad那么?&#34;。不幸的是,逆差异并不适用于monad(参见Are there contravariant monads?),但有f
的逆变类似物:https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html
3 请注意,我的Reader
在此处与Haskell中实现的a -> r
中的实际a
不匹配;您不能仅仅因为Applicative
不是contramap'
的最后一个类型参数而使contramap
成为Haskell代码中Contravariant
的实际实例。 从概念上来说它运行得非常好,你总是可以使用newtype包装来交换类型参数并使它成为一个实例(逆变器为这个目的定义了a -> r
类型。)< / p>
4 至少对于#34;序列化&#34;的定义这并不一定包括以后能够重建Bar,因为它会将一个Bar序列化为与它映射到的Foo相同,而无法包含有关映射的任何信息。
答案 1 :(得分:14)
首先,@ haoformayor的答案非常好,所以请考虑这个更多的附录而不是完整的答案。
我喜欢考虑Functor(co / contravariant)的一种方式是图表。该定义反映在以下内容中。 (我正在使用contramap
)缩写cmap
covariant contravariant
f a ─── fmap φ ───▶ f b g a ◀─── cmap φ ─── g b
▲ ▲ ▲ ▲
│ │ │ │
│ │ │ │
a ────── φ ───────▶ b a ─────── φ ──────▶ b
注意:这两个定义中唯一的变化是顶部的箭头(以及名称,所以我可以将它们称为不同的东西)。
在谈到这些是函数时,我总是有一个例子 - 然后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
是唯一的正确的选择。
这反映在下面的图表中,但是在这里我们已经对具有共同源/目标的函数的示例进行了抽象 - 具有协变/逆变属性的东西,这是你经常在数学中看到的东西和/或者哈斯克尔。
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 (ψ.φ)
答案 2 :(得分:13)
您可以将Functor f
视为a
永远不会出现在&#34;负面位置&#34;的断言。这是这个想法的一个深奥术语:请注意,在以下数据类型中,a
似乎充当了&#34;结果&#34;变量
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
代表&#34;结果&#34;一个功能。将第二个示例中的a
视为() -> a
可能会有所帮助。并且可能有助于记住第三个示例等同于data List a = Nil | Cons a (List a)
。在a -> List -> r
之类的回调中,a
出现在负位置,但回调本身处于负位置,因此负数和负数乘以正数。
用于签署函数参数的方案是elaborated in this wonderful blog post。
现在请注意,这些类型中的每一种都允许Functor
。那不是错!函数用于模拟分类协变函子的概念,它保留了箭头的顺序&#34;即f a -> f b
而不是f b -> f a
。在Haskell中,a
永远不会出现在负面位置的类型总是允许Functor
。我们说这些类型在a
上是协变的。
换句话说,可以有效地将Functor
类重命名为Covariant
。他们是同一个想法。
这个想法措辞如此奇怪的原因是&#34;从来没有&#34;是a
可以出现在正位置和负位置,在这种情况下我们说类型在a
上是不变的。 a
也可能永远不会出现(例如幻像类型),在这种情况下,我们说a
上的类型是协变和逆变 - bivariant。
因此,对于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
。
非常详细!但解决新型引发的冗长问题必然是另一个问题......
答案 3 :(得分:0)
我知道这个答案不会像其他答案那样学术性强,但这只是基于您会遇到的逆变量的常见实现。
首先,一个技巧:不要像阅读优秀的Functor的contraMap
那样使用f
的心理隐喻来读取map
函数类型。
您知道自己的想法:
“ 包含(或产生)的
t
东西”
...当您读到类似f t
的类型时?
在这种情况下,您需要停止这样做。
对立函子是经典函子的“对偶”,因此,当您在f a
中看到contraMap
时,应该想到“双重”隐喻:
f t
是消费 at
现在contraMap
的类型应该开始有意义了:
contraMap :: (a -> b) -> f b ...
...在那儿暂停一下,类型非常合理:
b
的功能。b
的东西。第一个参数会烹饪b
。第二个参数占用b
。
有道理吧?
现在完成输入类型:
contraMap :: (a -> b) -> f b -> f a
因此,最后这件事必须产生一个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
)。答案 4 :(得分:0)
关于该主题的另一种观点,仅限于被视为逆变函子的函数。 (另见this。)
Functor
s)f
类型的函数 a -> b
可以被认为包含 b
类型的值,当我们提供 a
类型的值时,我们可以访问该值到f
。
现在,作为其他事物的容器的事物可以变成 Functor
,从某种意义上说,我们可以通过将 g
应用于函子来将函数 fmap g
应用于它们的内容自己。
因此,f
类型的 a -> b
可以看作是 b
中的函子,即 (->) a
可以变成 Functor
。为此,我们需要定义fmap
:fmap
在g
的“内容”上ping一个函数f
本质上意味着将g
应用于任何{{ 1}} 返回(显然,一旦它输入了 f
类型的输入),这意味着 a
,或者更简洁地说,fmap g f = \x -> g (f x)
。
fmap g f = g . f
ping fmap
函数 = 后处理他们的 a -> b
类型的结果最后一个想法:b
类型的函数是 a -> b
中的函子,因为我们可以发布-处理它函数 b
(其中 b -> c
只是另一种类型)。
c
ping contramap
函数 = 预处理一个输入以获得a -> b
但是,如果我们想使用函数 a
(类型为 g
)来预处理-处理某种类型的值,该怎么办? {1}} 获取我们想要提供给 c -> a
的 c
类型的值?
嗯,很明显,在这种情况下,我们希望 a
在 f
之前采取行动,即我们正在寻找 g
。
而且我们希望 f
成为“在 f . g
上映射 f . g
” 概念的“实现”。换句话说,我们想要 g
。
你猜怎么着? f
其实就是函数whichmap g f = f . g
的实现!而 whichmap
是您必须实现的,以使某种类型成为 contramap
函子类型类的实例。
contramap
...实际上并不完全有 Contravariant
的 (-> b)
镜像 instance
,我相信只是因为 Contravariant
/instance Functor ((->) r)
是无效语法;因此创建了另一种类型,通过
instance Contravariant (-> r)
和这个是instance Contravariant (flip (->) r)
的一个实例:
newtype Op a b = Op { getOp :: b -> a }
最后两块代码取自hackage。
this 页面顶部的示例也很有启发性。