1

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?

Zhiltsoff Igor
  • 1,812
  • 8
  • 24
  • `phantom` is not a method of either `Functor` or `Contravariant`. You don't need "rewrite it twice" or even once. – Fyodor Soikin Jul 22 '21 at 20:43
  • 1
    @FyodorSoikin You do, to implement `fmap` and `contramap`, which are `const phantom`. Sorry for bad wording – Zhiltsoff Igor Jul 22 '21 at 20:51
  • 1
    Well, there's, like... `Proxy` and `Const` and... what else, exactly? (Indeed, every phantom type is `Const` in disguise.) You're saving maybe five seconds of programmer time like once every ten years. Feels hard to get worked up over that. – Daniel Wagner Jul 23 '21 at 13:53
  • Hm would this be safe: `phantom = unsafeCoerce :: Functor f => Contravariant f => f a -> f b`. – Iceland_jack Jul 16 '22 at 10:16

3 Answers3

6

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.

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • Huh, that’s interesting effect. I didn’t know about it, thank you. Anyway, even if we drop those instances, wouldn’t it still be handier to have a chance to implement `phantom` in own terms? Idiomatically speaking, phantom variance is a separate case both from co- and contravariance – Zhiltsoff Igor Jul 22 '21 at 21:05
  • How would you imagine doing that? Would yo write `class (Functor f, Contravariant f) => Phantom f where ...`? What would be the point in that? – amalloy Jul 22 '21 at 21:09
  • Defining `Phantom` just like we define the rest - without any extensions. I. e. `class Phantom f where phantom :: f a -> f b`. Even though the idea behind the implementation is cool, I cannot see much point in it: (sticking to default `<$` and `$<`) we run both `fmap` and `contramap`, which are the same as `const phantom`. I. e. to run `phantom` we run `const phantom` **twice** via two **separate** functions – Zhiltsoff Igor Jul 22 '21 at 21:14
  • @ZhiltsoffIgor The problem with that is that you can then only use `phantom` on a type if that type's author has written the Phantom instance. And authors usually won't. You'll end up wanting the standalone `phantom` function anyway, for authors who don't declare an instance. Using the class version would be slightly more efficient but less general. – amalloy Jul 23 '21 at 02:16
  • why do we assume the author would write `Functor` and `Contravariant` instances? Besides, what is stopping us from implementing an instance ourselves? – Zhiltsoff Igor Jul 23 '21 at 09:58
  • If we're allowed to change `base` we can do better than overlapping: we can use `DefaultSignatures` and write `class Functor f where fmap :: (a -> b) -> f a -> f b; default fmap :: Phantom f => (a -> b) -> f a -> f b; fmap _ = phantom`. ...but if we're going to do something like that, the `foldMapDefault` from `Data.Traversable` seems like it would apply more often. It is strictly more general in the sense that any phantom type can be made traversable. – Daniel Wagner Jul 23 '21 at 13:56
  • @ZhiltsoffIgor, most authors do write `Functor` instances when they can, at least for exported types. `Contravariant` is less universal, but still reasonably popular. But phantom type variables are very often used internally, and when exposed are often artificially forced to `nominal`, with neither `Functor` nor `Contravariant` Instances. The main exception I can think of is `Const`, for which the function version is just fine. – dfeuer Jul 23 '21 at 13:59
  • @DanielWagner, oh, good point about `Traversable`! – dfeuer Jul 23 '21 at 17:07
  • @DanielWagner Can you, please elaborate on why you call `Traversable` strictly more general? Doesn't `Traversable` extend `Functor`? Every `Traversable` instance can be made into a `Functor` via [`fmapDefault`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Data-Traversable.html#v:fmapDefault) (thus, if every phantom type can be made into a `Traversable`, the same goes for `Functor`). I guess I'm missing which *generality* it is. – Zhiltsoff Igor Jul 24 '21 at 06:35
  • 1
    @ZhiltsoffIgor A default for `fmap` that works for any `Traversable` is more general than a default for `fmap` that works for any `Phantom`, because any `Phantom` can be made `Traversable`. – Daniel Wagner Jul 24 '21 at 14:53
2

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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
Noughtmare
  • 9,410
  • 1
  • 12
  • 38
0

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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • Sorry, I can’t edit the question right now. For now, can you, please answer the question I asked concerning the usefulness of this default implementation under @amalloy’s post? I will add it to the post later – Zhiltsoff Igor Jul 22 '21 at 21:21
  • @ZhiltsoffIgor, I really don't understand what you're getting at with `const phantom`; maybe amalloy can answer your question. – dfeuer Jul 22 '21 at 21:25