7
votes

This answer from a Category Theory perspective includes the following statement:

...the truth is that there's no real distinction between co and contravariant functor, because every functor is just a covariant functor.

...

More in details a contravariant functor F from a category C to a category D is nothing more than a (covariant) functor of type F : Cop→D, from the opposite category of C to the category D.

On the other hand, Haskell's Functor and Contravariant merely require fmap and contramap, respectively, to be defined for an instance. This suggests that, from the perspective of Haskell, there exists objects that are Contravariant but are not Functors (and vice versa).

So it seems that in Category Theory "there's no real distinction between co and contravariant functors" while in Haskell there is a distinction between Contravariant and Functor.

I suspect that this difference has something to with all implementation in Haskell happening in Hask, but I'm not sure.

I think I understand each of the Category Theory and Haskell perspectives on their own, but I'm struggling to find an intuition that connects the two.

3
It's been a long time since I studied category theory (it was never an area I specialised in, anyway), but I think it's safe to say it's not true that "there's no difference" between covariant and contravariant functors. It's just that one can be defined in terms of the other, so in mathematical terms the difference is trivial. (Which is not to say that it can't have non-trivial effects in applications to other areas of mathematics.) Now I'm relatively new to Haskell and not familiar with the Contravariant class, but I imagine it faithfully reproduces the mathematical definition.Robin Zigmond
so having thought a little more, the reason that Functor and Contravariant are not "the same" in Haskell is probably just because the opposite category to Hask (one in which a morphism from a to b is represented by a function of type b -> a) is just not something that's particularly natural to think about. This is because Haskell is really all about just one category, and functors are endofunctors on Hask, whereas mathematical category theory is about the relationships (ie. functors between) all sorts of different categories (which are individually important in their own right).Robin Zigmond
You could easily get by without the concept of contravariant functors, because any such functor is just a covariant functor given the appropriate source category. It's easier to define a Contravariant typeclass, though, than it is to define Haskell<sup>OP</sup>.chepner

3 Answers

10
votes

It's for convenience.

One could get by with a more general Functor class, and define instances for endofunctors on Hask (corresponding to our existing Functor) and functors from Hask^op to Hask (corresponding to our existing Contravariant). But this comes at a figurative cognitive cost and a quite literal syntactical cost: one must then rely on type inference or type annotations to select an instance, and there are explicit conversions (named Op and getOp in the standard library) into and out of Hask^op.

Using the names fmap and contramap relaxes both costs: readers do not need to run Hindley-Milner in their head to decide which instance is being selected when it is unambiguous, and writers do not need to give explicit conversions or type annotations to select an instance in cases where it is ambiguous.

(I am actually rewriting history a little bit here. The real reason is because the language designers thought the specialized Functor would be useful and hadn't imagined or didn't see a need for a more general Functor. People came along later and noticed it would be useful, sometimes. But experience with the generalized Functor class shows that can be tedious, and that specialized classes for the most common cases turns out to be a surprisingly good fit after all, for the reasons described above.)

7
votes

Imagine for a minute we had something like the following.

class MoreAccurateFunctor c d f where
  fmap :: c a b -> d (f a) (f b)

Since (->) is an instance of Category (this is Hask), we would have that Functor ~ MoreAccurateFunctor (->) (->).

Now, imagine we have Dual (->), the dual category of (->) (this would be HaskOp and we would have Dual (->) a b ~ (b -> a)), we would have that Contravariant ~ MoreAccurateFunctor (Dual (->)) (->).

I don't know if this helps but the idea is to point out the fact that Functor and Contravariant are two specialisations of MoreAccurateFunctor while this latter class is closer to the definition of functor in category theory.

6
votes

Mathematically, considering contravariant functors as a distinct class of functors is just a notational convenience; the contravariant functor F : C -> D can always be defined as a covariant functor F' : C^{op} -> D, so getting rid of the idea of contravariant functors would just force you to talk about the opposite category explicitly.

In Haskell, the Functor class represents an endofunctor on the (assumed) category Hask. There is no convenient way to represent HASKOP directly (or at least, not in a form that helps us define functors from that category), nor is there a typeclass that defines exofunctor*, so instead we define the Contrafunctor class whose contramap function can reverse the arrow from Hask "on demand", so to speak.


* Is "exofunctor" a real term? I just made it up to indicate a functor that is not an endofunctor.