This Reddit post by Edward Kmett provides a constructive definition of a natural map, the one from the free theorem for fmap (which I read in yet another Edward Kmett's post):
For given
f,g,handk, such thatf . g = h . k:$map f . fmap g = fmap h . $map k, where$mapis the natural map for the given constructor.
I do not fully understand the algorithm. Let us approach it step-by-step:
We can define a "natural map" by induction over any particular concrete choice of
Fyou give. Ultimately any such ADT is made out of sums, products,(->)'s,1s,0s,a's, invocations of other functors, etc.
Consider:
data Smth a = A a a a | B a (Maybe a) | U | Z Void deriving ...
No arrows. Let us see how fmap (which I reckon to be the natural choice for any ADT without (->)s in it) would operate here:
instance Functor Smth where
fmap xy (A x x1 x2) = A (xy x) (xy x1) (xy x2)
fmap xy (B x xPlus1) = B (xy x) (fmap xy xPlus1)
-- One can pattern-match 'xPlus1' as 'Just x1' and 'Nothing'.
-- From my point of view, 'fmap' suits better here. Reasons below.
fmap _xy U = U
fmap _xy (Z z) = absurd z
Which seems natural. To put this more formally, we apply xy to every x, apply fmap xy to every T x, where T is a Functor, we leave every unit unchanged, and we pass every Void onto absurd. This works for recursive definitions too!
data Lst a = Unit | Prepend a (Lst a) deriving ...
instance Functor Lst where
fmap xy Unit = Unit
fmap xy (Prepend x lstX) = Prepend (xy x) (fmap xy lstX)
And for the non-inductive types:(Detailed explanation in this answer under the linked post.)
Graph a = Node a [Graph a]
instance Functor Graph where
fmap xy (Node x children) = Node (xy x) (fmap (fmap xy) children)
This part is clear.
When we allow
(->)we now have the first thing that mixes variance up. The left-hand type argument of(->)is in contravariant position, the right-hand side is in covariant position. So you need to track the final type variable through the entire ADT and see if it occurs in positive and/or negative position.
Now we include (->)s. Let us try to keep this induction going:
We somehow derived natural maps for T a and S a. Thus, we want to consider the following:
data T2S a = T2S (T a -> S a)
instance ?Class? T2S where
?map? ?? (T2S tx2sx) = T2S $ \ ty -> ???
And I believe this to be the point where we start choosing. We have the following options:
- (Phantom)
aoccurs neither inTnor inS.ainT2Sis phantom, thus, we can implement bothfmapandcontramapasconst phantom. - (Covariant)
aoccurs inS aand does not occur inT a. Thus, this something among the lines ofReaderTwithS a(which does not actually depend ona) as environment, which substitutes?Class?withFunctor,?map?withfmap,???,??withxywith:let tx = phantom ty sx = tx2sx tx sy = fmap xy sx in sy - (Contravariant)
aoccurs inT aand does not occur inS a. I do not see a way to make this thing a covariant functor, so we implement aContravariantinstance here, which substitutes?map?withcontramap,??withyx,???with:let tx = fmap yx ty sx = tx2sx tx sy = phantom sx in sy - (Invariant)
aoccurs both inT aandS a. We can no longer usephantom, which came in quite handy. There is a moduleData.Functor.Invariantby Edward Kmett. It provides the following class with a map:
And yet, I cannot see a way to turn this into something we can pluf into the free theorem for fmap - the type requires an additional function-argument, which we can't brush off asclass Invariant f where invmap :: (a -> b) -> (b -> a) -> f a -> f b -- and some generic stuff...idor something. Anyway, we putinvmapinstead of?map?,xy yxinstead of??, and the following instead of???:let tx = fmap yx ty sx = tx2sx tx sy = fmap xy sx in sy
So, is my understanding of such an algorithm correct? If so, how are we to properly process the Invariant case?