Note: The current question is a follow-up to this one. It received a great answer by Daniel Wagner.
I am trying to figure out how to derive a natural map for the given type of kind * -> *. natmap is a natural map for F, if it is equal to:
fmap :: (a -> b) -> F a -> F bin case of covariantF,contramap :: (b -> a) -> F a -> F bin case of contravariantF,invmap :: (a -> b) -> (b -> a) -> F a -> F bin case of invariantF,- or
phantom :: F a -> F bin case of phantomF.
To summarize, Daniel Wagner proposed an algorithm which derives fmap if the given functor is covariant or fails otherwise. The same could be done for contramap and invmap. They implemented fmap and contramap:
-- COVARIANT
fmap @(F + F') f (Left x) = Left (fmap @F f x)
fmap @(F + F') f (Right x) = Right (fmap @F' f x)
fmap @(F * F') f (x, y) = (fmap @F f x, fmap @F f y)
fmap @(Id) f x = f x
fmap @(Const X) f x = x
fmap @(F -> F') f x = fmap @F' f . x . contramap @F f
fmap @(F . F') f x = fmap @F (fmap @F' f) x
-- OR
fmap @(F . F') f x = contramap @F (contramap @F' f) x
-- CONTRAVARIANT
contramap @(F + F') f (Left x) = Left (contramap @F f x)
contramap @(F + F') f (Right x) = Right (contramap @F' f x)
contramap @(F * F') f (x, y) = (contramap @F f x, contramap @F' f y)
-- contramap @(Id) fails
contramap @(Const X) f x = x
contramap @(F -> F') f x = contramap @F' f . x . fmap @F f
contramap @(F . F') f x = contramap @F (fmap @F' f) x
-- OR
contramap @(F . F') f x = fmap @F (contramap @F' f) x
Daniel Wagner's answer got me thinking. It must be possible to substitute three algorithms (which derive fmap, contramap and invmap) with a single algorithm, which derives invmap. E. g.: we derived invmap for data F a = F (a, a) and while doing so figured out that F is covariant. Thus, we can define fmap f = invmap f undefined for F. And vice versa for data F' = F' ((a, a) -> Int): contramap g = invmap undefined g.
How Is invmap Derived?
Basically, we adjust Daniel Wagner's algorithm:
{- 1 -} invmap @(F + F') f g (Left x) = Left (invmap @F f g x)
{- 2 -} invmap @(F + F') f g (Right x) = Right (invmap @F' f g x)
{- 3 -} invmap @(F * F') f g (x, y) = (invmap @F f g x, invmap @F' f g y)
{- 4 -} invmap @(F -> F') f g x = invmap @F' f g . x . invmap @F g f
{- 5 -} invmap @(F . F') f g x = invmap @F (invmap @F' f g) (invmap @F' g f) x
{- 6 -} invmap @(Id) f g x = f x
{- 7 -} invmap @(Const X) f x = x
A few comments concerning some of the lines above:
5 The idea is that in invmap @F (invmap @F' a b) (invmap @F' c d):
aprocesses covariant positions inFwhich appear in covariant positions inF',bprocesses contravariant positions inFwhich appear in covariant positions inF',cprocesses covariant positions inFwhich appear in contravariant positions inF',dprocesses contravariant positions inFwhich appear in contravariant positions inF'.
I. e. a and d process covariant positions, whereas b and c process contravariant positions. Thus, a = d = f; b = c = g.
6 Id leafcase indeed fails for contravariant functors as, when contramap g is called, undefined is put in f's place. Dually: Id leafcase is the only one to use f directly for covariant functors.
Why Does It Work?
I reckon this algorithm to work due to a simple reason: invmap does not actually have to use each of its arguments directly. Besides, when we try using the covaraint processor for a contravariant functor (i. e. running contramap @Id g x), we get an error (as the covariant processor is undefined).
Basically, I the idea came from implementing fmap and contramap in terms of invmap, just like phantom is implemented in terms of fmap and contramap.
Why Is It Interesting?
Branching produces some overheat. E. g.:
contramap @(F . F') f x = contramap @F (fmap @F' f) x
-- OR
contramap @(F . F') f x = fmap @F (contramap @F' f) x
Unless F and F' are phantom, one of the cases would have failed after examining the whole structure of F and F'. One may claim that choosing one out of two does not really classify as overheat and can be pardoned in order to maintain readability. But things get quite ugly with invmap. I consider there to be 7 subcases for each of the following cases: F -> F', F + F', F * F'. Such code seems to be quite inefficient.
All those subcases were generated by our assumptions concerning the variance. When we stop doing that deriving invmap only, the branching disappears.
The Question
Does this algorithm yield lawful natural maps? I don't think we can prove that fmap f = invmap f undefined for lawful instances. Rather, we depend on the fact that we run the contravariant processor iff we encounter a contravariant position (same for contravariant), which is the case in the algorithm above.
P. S. Some suggest the Invariant interface is inferior to Profunctors and should be avoided. But, if my algorithm is indeed correct, I think this is a pretty good use for such a class.