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 b
in case of covariantF
,contramap :: (b -> a) -> F a -> F b
in case of contravariantF
,invmap :: (a -> b) -> (b -> a) -> F a -> F b
in case of invariantF
,- or
phantom :: F a -> F b
in 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)
:
a
processes covariant positions inF
which appear in covariant positions inF'
,b
processes contravariant positions inF
which appear in covariant positions inF'
,c
processes covariant positions inF
which appear in contravariant positions inF'
,d
processes contravariant positions inF
which 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 Profunctor
s and should be avoided. But, if my algorithm is indeed correct, I think this is a pretty good use for such a class.