Discussion around (limitations of) Coercible, GeneralizedNewtypeDeriving or DerivingVia resume once in a while.
One common question is whether fmap coerce = coerce should be a law of the Functor type-class, or not. Intuitively, it's similar to the identity law: fmap id = id law, but intuition is not good enough reason alone.
The identity law comes from category theory, so what could category theory say about fmap coerce = coerce like proposition?
Let us consider an endofunctor from a three element category to itself, which "rotates the category".

maps objects to respectively. Careful reader would ask how maps morphisms. See an appendix, for a justification.
To make it more concrete we can think that as:
type A = Int
type B = Word
newtype B' = B' Wordwith isomorphisms in between (assuming Int and Word have the same amount of bits).
Then, we know that Coercible B B'. And arrows and on the diagram are coerce @B @B' and coerce @B' @B. But Coercible (F B) (F B') = Coercible B' A doesn't exist. is a function like \(B' w) -> fromIntegral w which is not coerce.
The functor doesn't preserve the Coercible relation, even it's a categorically lawful functor.
But the functor above is not a Functor as in Haskell. Not even if we try to define
data F :: Type -> Type where
FA :: A -> F A
FB :: B -> F B
FB' :: B' -> F B'we'll not be able to write fmap :: (a -> b) -> F a -> F b, as b can be anything, not only A, B or B'.
fmap is polymorphically parametric. There are papers and blogs explaining parametricity1.
The key insight of parametricity is that we can read types as relations. For example a list defined as
data List x = Nil | Cons x (List x)can be read as a relation constructor:
Given a relation R :: Relation X Y, xs :: List X and ys :: List Y are related with List R :: Relation (List X) (List Y), if either
xs = Nil and ys = Nil, orxs = Cons x xs', ys = Cons y ys', and x and y are related by R, and xs' and ys' are recursively related by List R.In other words, List R relates lists of equal length, when all elements are related pointwise.
We can derive free theorems for polymorphic functions over lists, or ...
We could take R X Y to be Coercible X Y. Relation doesn't need to be functional. Coercible is a binary-relation. We can reason that List Coercible is Coercible for lists: Lists related by coerce @(List X) @(List Y) are of the same length and each element is related by coerce @X @Y.
Maybe instead of asking whether fmap coerce = coerce should be a law of the Functor type-class, we should ask whether Functor should be a type-class of parametric type constructors? Parametricity would imply fmap coerce = coerce. Non-requirement of parametricity, would mean that opaque types can be "lawfully" Functor, e.g. similarly as Set a has an Eq instance.
The latter choice allows us to write type internally used in the bifunctors library:
data Mag a b t where
Pure :: t -> Mag a b t
Map :: (x -> t) -> Mag a b x -> Mag a b t
Ap :: Mag a b (t -> u) -> Mag a b t -> Mag a b u
One :: a -> Mag a b b
instance Functor (Mag a b) where
fmap = Map
instance Applicative (Mag a b) where
pure = Pure
(<*>) = ApMag is not lawful Functor, it violates fmap id = id property. but is used to implement traverseBiaWith.
However, there is a comment in the code:
-- Note: if it's necessary for some reason, we *could* relax GADTs to
-- `ExistentialQuantification` by changing the type of One to
--
-- One :: (b -> c) -> a -> Mag a b cThen Mag will be Coercible in all argument, current Mag has type role Mag representational nominal nominal.
A third alternative is to use
One :: Coercible b c => a -> Mag a b cGHC is smart enough to infer that this Mag variant is also representational in all arguments. Unfortunately, to implement traverseBiaWith, we'll need to change the required constraints
- Biapplicative p
+( Biapplicative p
+, forall u v x y. (Coercible u x, Coercible v y)
+ => Coercible (p u v) (p x y))
+)... or require Bifunctor (a superclass of Biapplicative) to be parametric in its both arguments!
For me, it looks like that the addition of (forall x y. Coercible x y => Coercible (f x) (f y)) constraint to Functor (and similarly for Bifunctor, Profunctor, Contravariant ...) can be worked around in all cases. The Mag is the only example mentioned as useful but not lawful Functor, 2 and we have demonstrated a required change.
Note: Mag will still be unlawful, but it can be made representational in all arguments. In the implementation of traverseBiaWith we will use coerce which is free operationally, so there shouldn't be any performance degradation.
I cannot come up with an example of f :: Type -> Type which would violate fmap id = id law, but obey fmap coerce = coerce one (or an opposite direction). And I cannot show that fmap coerce = coerce follows from other two Functor laws, but without using parametricity. Edward Kmett explains how Functor composition law follows from identity law and parametricity. Coerce law is implied by parametricity directly.
Finally, my conclusion is that both
fmap coerce = coerce should be a law of GHC Haskell Functor, andforall a b. Coercible a b => Coercible (f a) (f b) be Functor super-constrainteven the change departs us far far from Haskell2010. The justification is that these requirements are implied by parametricity, and Functor class should contain only parametric type constructors. We would still be able to have unlawful Functors if really needed.
Some immediate benefits are ability to put join into Monad or that van Laarhoven Lens s t a b would be representable in all arguments.
The disadvantage is that there's Haskell 2010 code which would be broken by that change. Consider
data Eq a => Foo aWe can write (unlawful) Functor instance in Haskell 2010, which won't be accepted, as Foo role is nominal. Yet, GHC documentation about DatatypeContexts says This is widely considered a misfeature, and is going to be removed from the language. You need to enable a language feature to write Foo, in other words that code is already broken (since November 2010!)
Also a non-Haskell2010 code will be broken:
type family Id (x :: Type) :: Type where
Id x = x
newtype I x = I (Id x)You may think, there's an easy solution, but there is a five year old issue about roles for type families. Also it have to be investigated, if someone actually wrote an (unlawful) Functor wrapping a type family which pattern matches (i.e. is non-parametric) on an argument, and why they did that!
Just adding fmap coerce = coerce law wouldn't break any code. Something which is unlawful will be a bit more unlawful. The hypothesis is that fmap coerce = coerce won't make any currently lawful Functors into unlawful one.
I'm thankful to Ryan Scott for many discussions and valuable insights. And to Andres Löh for comments on a draft of this post.
In this category there is an unique arrow between any two objects, so the mapping is trivial.
We can extend the category by adding more objects and morphisms, and so it keeps all other objects in place. In this case mapping of morphisms is trickier. If we require that and others identities suggested by names or arrows we can make it work.
Let us define two families of morphisms indexed by objects in : and
Using these, we can define mapping of morphisms as
For example is mapped to
Check by looking at the diagram!
The identity and composition properties hold, for example
Some links about parametricity
↩︎For example in https://ryanglscott.github.io/2018/06/22/quantifiedconstraints-and-the-trouble-with-traversable/↩︎