Why not a Phantom class which extends Functor Contravariant?

200 Views Asked by At

I am playing around with the Data.Functor.Contravariant. The phantom method caught my eye:

phantom :: (Functor f, Contravariant f) => f a -> f b
phantom x = () <$ x $< ()

Or, more specifically, the annotation to it:

If f is both Functor and Contravariant then by the time you factor in the laws of each of those classes, it can't actually use it's argument in any meaningful capacity. This method is surprisingly useful. Where both instances exist and are lawful we have the following laws: fmap f ≡ phantom, contramap f ≡ phantom

Since fmap f ≡ contramap f ≡ phantom, why do we need Contravariant and Functor instances? Isn't it handier to do this thing the other way: create an instance for one class Phantom, which introduces the phantom method, and then automatically derive instances for Functor and Contravariant?

class Phantom f where
    phantom :: f a -> f b
instance Phantom f => Functor f where
    fmap _f = phantom

instance Phantom f => Contravariant f where
    contramap _f = phantom

We will rid the programmer of the necessity to rewrite this phantom twice (to implement fmap and contramap, which are const phantom, as stated in the annotation) when implementing instances for Contravariant and Functor. We will allow writing one instance instead of two! Besides, it seems nice and idiomatic to me to have classes for all 4 cases of variance: Functor, Contravariant, Invariant (yet, some suggest using Profunctor interface instead of Invariant), and Phantom.

Also, isn't it a more efficient approach? () <$ x $< () requires two traverses (as much as we can traverse a phantom functor...), as long as the programmer might carry this transformation out a bit faster. As far as I understand, the current phantom method can't be overridden.

So, why didn't the library developers choose this way? What are the pros and cons of the current design and the design I spoke of?

3

There are 3 best solutions below

3
Noughtmare On BEST ANSWER

To avoid the overlapping instances mentioned by amalloy you could define a newtype which can be used with DerivingVia:

{-# LANGUAGE DerivingVia #-}

import Data.Functor.Contravariant hiding (phantom)

class (Functor f, Contravariant f) => Phantom f where
  phantom :: f a -> f b

newtype WrappedPhantom f a = WrappedPhantom (f a)

instance Phantom f => Phantom (WrappedPhantom f) where
  phantom (WrappedPhantom x) = WrappedPhantom (phantom x)

instance Phantom f => Functor (WrappedPhantom f) where
  fmap _ = phantom

instance Phantom f => Contravariant (WrappedPhantom f) where
  contramap _ = phantom

-- example of usage:

data SomePhantom a = SomePhantom
  deriving (Functor, Contravariant) via WrappedPhantom SomePhantom

instance Phantom SomePhantom where
  phantom SomePhantom = SomePhantom

It's not quite as convenient as having the instances automatically, but it still means that you don't have to implement Functor and Contravariant instances manually.

10
amalloy On

There are many types which are an instance of Functor but not of Phantom, and likewise Contravariant. For such types, the structure you propose would be a big problem because of overlapping instances.

instance Phantom f => Functor f

does not mean "if f is a Phantom then it is also a Functor". Only instance heads are searched during typeclass resolution, and the constraints come in later. This is related to the open world assumption. So you are declaring a Functor instance for f, a totally unconstrained type variable which will overlap with every other possible instance declaration.

2
dfeuer On

The best you could really do would be something like this:

class (Functor f, Contravariant f) => Phantom f where
  phantom :: f a -> f b
  phantom x = () <$ x $< ()

The trouble is that people probably won't be interested in taking the time to instantiate the class.