什么是逆变函子? [英] What is a contravariant functor?

查看:107
本文介绍了什么是逆变函子?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

  class Contravariant(f :: *  - > *)其中
contramap ::(a - > b) - > f b - > fa

然后我阅读,但与标题相反,我没有更多的开明。



有人可以给一个解释逆变函数functor是和一些例子吗?

解决方案

从程序员的角度来看,函子的本质是能够轻松适应的东西。我在这里适应的意思是,如果我有一个 fa ,并且我需要一个 fb ,我会就像一个适配器,它适合我的 fa 在我的 fb 形孔中。



看起来很直观,如果我可以将 a 变成 b ,那我可能会能够将 fa 变成 fb 。确实就是Haskell的 Functor 类体现的模式;如果我提供 a - > b 函数然后 fmap 让我把 fa 变成 fb 的东西,而不必担心任何 f 涉及。 1



<当然,讨论参数列表类型,比如list-of-x [x] 也许y 或者 IO z 在这里,我们可以用适配器改变的是 x y z 。如果我们想要灵活地从任何可能的函数获取适配器 a - > b 那么我们正在调整的东西必须同样适用于任何可能的类型。



什么是不太直观的(起初)是有一些类型可以调整几乎完全相同的方式作为有趣的,只有他们是倒退;对于这些如果我们想调整 fa 来满足 fb 的需求,我们实际上需要提供一个 b - >一个函数,而不是 a - > b one!

我最喜欢的具体例子实际上是函数类型 a - > r (a表示参数,r表示结果);所有这些抽象的废话在应用于功能时都是非常有意义的(如果你已经做了任何实质性的编程,你几乎可以肯定地使用这些概念而不知道术语或者它们有多广泛适用),并且这两个概念是如此明显在这种情况下彼此是双重的。

众所周知, a - > r r 中的函子。这是有道理的;如果我有 a - > r ,我需要一个 a - > s ,那么我可以使用 r - > s 函数可以简单地通过后处理结果来调整我的原始函数。 2



如果在另一个手,我有一个 a - > r 函数,我需要的是 b - > r ,那么很明显,我可以通过预处理参数来解决我的需求,然后再将它们传递给原始函数。但是,我要如何预处理它们?原始功能是黑匣子;不管我做什么,总是期待 a 输入。所以我需要将我的 b 值转换为它所期望的 a 值:我的预处理适配器需要一个 b - >一个函数。



我们刚刚看到的是函数类型 a - > r r 中的一个协变函子,<$ c中有一个逆变函数 $ C> A 。我认为这是因为我们可以调整函数的结果,并且结果类型随着适配器变化 r - > s ,而当我们调整函数的参数时,参数类型向适配器改变相反的方向。

有趣的是,函数结果 fmap 和函数参数 contramap 几乎完全一样:只是函数组合(运算符)!唯一的区别是你在哪一方组成适配器功能: 3

  fmap ::(r - > s) - > (a→r)→> (a  - > s)
fmap适配器f =适配器。 f
fmap adapter =(adapter。)
fmap =(。)

contramap'::(b - > a) - > (a→r)→> (b - > r)
contramap'adapter f = f。适配器
contramap'adapter =(。adapter)
contramap'= flip(。)



我认为每个区块的第二个定义是最有洞察力的; (协变)映射到一个函数的结果上是左边的合成(如果我们想要采用this-happen-after-that视图,则在合成之后),而在函数的参数上反向映射是右边的合成组合)。

这种直觉总体上很好,如果 fx 结构可以给我们类型 x 的值(就像 a - > r 函数为我们提供了 r 值,至少可能),它可能是一个协变 Functor x 中,我们可以使用 x - > y 函数将其调整为 f y 。但是,如果 fx 结构从我们那里接收到类型 x 值(同样, a - > r 函数的类型参数 a ),那么它可能是逆变形 functor,我们需要使用 y - > x 函数使它适应 fy



我觉得很有趣反映这种源是协变的,目的是逆变的,当你从源/目的地的实现者的角度思考直觉时,直觉反转而不是调用者。如果我试图实现接收 x 值的 fx ,我可以适应我自己的界面,所以我开始使用 y 值(同时仍然呈现接收 x 值界面给我的呼叫者)通过使用 x - > y 函数。通常我们不会这样想;即使作为 f x 的实现者,我也会考虑调整我所调用的内容,而不是调整我的调用者接口给我。但是这是你可以采取的另一种观点。



我使用 Contravariant (而不是通过使用右边的组合来隐含地使用函数中的函数的反变换,这是非常常见的)对于 Serialiser a 类型来说可以串行化 x 值。 Serialiser 必须是 Contravariant 而不是 Functor ;考虑到我可以连续化Foos,我还可以连续化酒吧,如果我可以 Bar - > Foo 4 但是,当你意识到 Serialiser a 基本上是 a - > ByteString 它变得明显;我只是重复的一个特例 - >>在纯粹的函数式编程中,没有太多的用途使得接收值的东西没有它也回馈所有的逆变函数都看起来像函数,但几乎任何可以包含任意类型值的简单数据结构都将是该类型参数中的协变函子。这就是为什么 Functor 很早就偷走了好名字的原因,并且被用在所有地方(当然, Functor 是被认为是 Monad 的一个基本部分,在 Functor 被定义为Haskell中的一个类之前已被广泛使用) 。在命令式OO中,我相信逆变函数可能更为常见(但不能像 Contravariant > 的标准容器)它既是可读写的,又是 a 的发射器和接收器,而不是意味着它既是协变又是反变量,这意味着它既不是)。






1 每个人的 Functor 实例 f 说明如何应用任意函数到 f 的特定形式,而不用担心特定类型 f 正被应用于;一个很好的分离问题。



2 这个函数也是一个monad,相当于 Reader monad。在这里我不打算超越函数,但是考虑到我的其余部分,一个明显的问题是是 a - > r 类型还有一些类型在 a 然后?中的逆变monad。不幸的是,逆变并不适用于monads(请参阅是否存在逆变monads?),但是应用型的逆变类似物: https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html



3 请注意,我的 contramap'与实际的 contramap 从 Contravariant 在Haskell中实现;你不能使 a - > r Haskell代码中 Contravariant 的实际实例仅仅是因为 a 不是最后一个键入( - >)的参数。从概念上讲,它可以很好地工作,并且你总是可以使用一个新类型的包装器来交换类型参数并且创建一个实例(逆变器定义了 Op 类型正是为了这个目的)。



4 至少对于serialise的定义,不一定包括能够重建后面的Bar,因为它会将Bar映射到与它相同的Foo,并且无法包含任何关于映射的信息。


The type blows my mind:

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

Then I read this, but contrary to the title, I wasn't any more enlightened.

Can someone please give an explanation of what a contravariant functor is and some examples?

解决方案

From a programmer's point of view the essence of functor-ness is being able to easily adapt things. What I mean by "adapt" here is that if I have an f a and I need an f b, I'd like an adaptor that will fit my f a in my f b-shaped hole.

It seems intuitive that if I can turn an a into a b, that I might be able to turn a f a into an f b. And indeed that's the pattern that Haskell's Functor class embodies; if I supply an a -> b function then fmap lets me adapt f a things into f b things, without worrying about whatever f involves.1

Of course talking about paramterised types like list-of-x [x], Maybe y, or IO z here, and the thing we get to change with our adaptors is the x, y, or z in those. If we want the flexibility to get an adaptor from any possible function a -> b then of course the thing we're adapting has to be equally applicable to any possible type.

What is less intuitive (at first) is that there are some types which can be adapted almost exactly the same way as functory ones, only they're "backwards"; for these if we want to adapt an f a to fill a need for a f b we actually need to supply a b -> a function, not an a -> b one!

My favourite concrete example is actually the function type a -> r (a for argument, r for result); all of this abstract nonsense makes perfect sense when applied to functions (and if you've done any substantial programming you've almost certainly used these concepts without knowing the terminology or how widely-applicable they are), and the two notions are so obviously dual to each other in this context.

It's fairly well known that a -> r is a functor in r. This makes sense; if I've got an a -> r and I need an a -> s, then I could use an r -> s function to adapt my original function simply by post-processing the result.2

If, on the other hand, I have an a -> r function and what I need is a b -> r, then again it's clear that I can address my need by pre-processing arguments before passing them to the original function. But what do I pre-process them with? The original function is a black box; no matter what I do it's always expecting a inputs. So I need to turn my b values into the a values it expects: my pre-processing adaptor needs a b -> a function.

What we've just seen is that the function type a -> r is a covariant functor in r, and a contravariant functor in a. I think of this as saying we can adapt a function's result, and the result type "changes with" the adaptor r -> s, while when we adapt a function's argument the argument type changes "in the opposite direction" to the adaptor.

Interestingly, the implementation of the function-result fmap and the function-argument contramap are almost exactly the same thing: just function composition (the . operator)! The only difference is on which side you compose the adaptor function:3

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 (.)

I consider the second definition from each block the most insightful; (covariantly) mapping over a function's result is composition on the left (post-composition if we want to take a "this-happens-after-that" view), while contravariantly mapping over a function's argument is composition on the right (pre-composition).

This intuition generalises pretty well; if an f x structure can give us values of type x (just like an a -> r function gives us r values, at least potentially), it might be a covariant Functor in x, and we could use an x -> y function to adapt it into being an f y. But if an f x structure receives values of type x from us (again, like an a -> r function's argument of type a), then it might be a Contravariant functor and we'd need to use a y -> x function to adapt it to being an f y.

I find it interesting to reflect that this "sources are covariant, destinations are contravariant" intuition reverses when you're thinking from the perspective of an implementer of the source/destination rather than a caller. If I'm trying to implement an f x that receives x values I can "adapt my own interface" so I get to work with y values instead (while still presenting the "receives x values" interface to my callers) by using an x -> y function. Usually we don't think this way around; even as the implementer of the f x I think about adapting the things I'm calling rather than "adapting my caller's interface to me". But it's another perspective you can take.

The only semi-real-world use I've made of Contravariant (as opposed to implicitly using the contravariance of functions in their arguments by using composition-on-the-right, which is very common) was for a type Serialiser a that could serialise x values. Serialiser had to be a Contravariant rather than a Functor; given I can serialise Foos, I can also serialise Bars if I can Bar -> Foo.4 But when you realise that Serialiser a is basically a -> ByteString it becomes obvious; I'm just repeating a special case of the a -> r example.

In pure functional programming, there's not very much use in having something that "receives values" without it also giving something back so all the contravariant functors tend to look like functions, but nearly any straightforward data structure that can contain values of an arbitrary type will be a covariant functor in that type parameter. This is why Functor stole the good name early and is used all over the place (well, that and that Functor was recognised as a fundamental part of Monad, which was already in wide use before Functor was defined as a class in Haskell).

In imperative OO I believe contravariant functors may be significantly more common (but not abstracted over with a unified framework like Contravariant), although it's also very easy to have mutability and side effects mean that a parameterised type just couldn't be a functor at all (commonly: your standard container of a that is both readable and writable is both an emitter and a sink of a, and rather than meaning it's both covariant and contravariant it turns out that means it's neither).


1 The Functor instance of each individual f says how to apply arbitrary functions to the particular form of that f, without worrying about the particular types f is being applied to; a nice separation of concerns.

2 This functor is also a monad, equivalent to the Reader monad. I'm not going to go beyond functors in detail here, but given the rest of my post an obvious question would be "is the a -> r type also some sort of contravariant monad in a then?". Contravariance doesn't apply to monads unfortunately (see Are there contravariant monads?), but there is a contravariant analogue of Applicative: https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html

3 Note that my contramap' here doesn't match the actual contramap from Contravariant as implemented in Haskell; you can't make a -> r an actual instance of Contravariant in Haskell code simply because the a is not the last type paramter of (->). Conceptually it works perfectly well, and you can always use a newtype wrapper to swap the type parameters and make that an instance (the contravariant defines the the Op type for exactly this purpose).

4 At least for a definition of "serialise" that doesn't necessarily include being able to reconstruct the Bar later, since it would serialise the a Bar identically to the Foo it mapped to with no way to include any information about what the mapping was.

这篇关于什么是逆变函子?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆