Skip to the content.

Dinatural transformations

When you have two functors, there may be morphisms between them in a category of functors, and some of them might be natural transformations. For morphisms to be natural transformations, they must satisfy

\[fmap \, a2b \circ nat_a \cong nat_b \circ fmap \, a2b\]

for any $a2b :: a \rightarrow b$. For example, let’s take Maybe and List as an example.

maybeToList :: Maybe a -> [a]
maybeToList (Just a) = [a]
maybeToList Nothing = []

For maybeToList to be a natural transformation, fmap a2b . maybeToList == maybeToList . fmap a2b. Let’s compare both sides. You can see both sides become the same value.

   (fmap a2b . maybeToList) Nothing
-> fmap a2b [] $ maybeToList Nothing
-> fmap a2b []
-> []

   (fmap a2b . maybeToList) (Just a)
-> fmap a2b [a] $ maybeToList (Just a)
-> fmap a2b [a]
-> [a2b a]
   (maybeToList . fmap a2b) Nothing
-> maybeToList $ fmap a2b Nothing
-> maybeToList Nothing
-> []

   (maybeToList . fmap a2b) (Just a)
-> maybeToList $ fmap a2b (Just a)
-> maybeToList (Just (a2b a))
-> [a2b a]

In the same token, there may be morphisms between two profunctors, and some of them might be natural transformations. For morphisms to be natural transformations, they must satisfy

\[\begin{align*} &\, rmap \, a2b \circ nat_{aa} \circ lmap \, a2b \\ \cong &\, lmap \, a2b \circ nat_{bb} \circ rmap \, a2b \\ \cong &\, rmap \, a2b \circ lmap \, a2b \circ nat_{ba} \\ \cong &\, lmap \, a2b \circ rmap \, a2b \circ nat_{ba} \end{align*}\]

for any $a2b :: a \rightarrow b$. Notice that all of them transform $p \, b \, a$ to $q \, a \, b$.

Natural transformations that satisfy only the first isomorphisms are called dinatural transformations.

\[rmap \, a2b \circ nat_{a} \circ lmap \, a2b \cong lmap \, a2b \circ nat_{b} \circ rmap \, a2b\]

While natural transformations transform a profunctor $p \, b \, a$ to a profunctor $q \, b \, a$, dinatural transformations transform only a profunctor $p \, a \, a$ to a profunctor $q \, a \, a$. They only care about diagonal elements. That’s why they’re indexed only by one object such as $nat_{a}$ instead of two ($nat_{aa}$).

For example, when you have two profunctors Pure and Star, you can have a dinatural transformation pureStar from the former to the latter.

newtype Pure a b = Pure (a -> b)

instance Profunctor Pure where
  dimap :: (s -> a) -> (b -> t) -> Pure a b -> Pure s t
  dimap s2a b2t (Pure f) = Pure (b2t . f . s2a)

newtype Star f a b = Star (a -> f b)

instance (Functor f) => Profunctor (Star f) where
  dimap :: (s -> a) -> (b -> t) -> (Star f a b -> Star f s t)
  dimap s2a b2t (Star a2fb) =
    Star
      ( \s ->
          let a = s2a s
              fb = a2fb a
              ft = fmap b2t fb
           in ft
      )
pureStar :: forall f a. (Applicative f) => Pure a a -> Star f a a
pureStar (Pure a2a) = Star a2fa
  where
    a2fa :: a -> f a
    a2fa = pure . a2a

Let’s expand both sides of the dinaturality condition to see if pureStar satisfies the dinaturality condition.

   (rmap a2b . pureStar . lmap a2b) (Pure b2a)
-> rmap a2b $ pureStar $ lmap a2b $ Pure b2a
-> rmap a2b $ pureStar $ Pure (b2a . a2b)
-> rmap a2b $ Star (pure . b2a . a2b)
-> Star (fmap a2b . pure . b2a . a2b)
-> Star (pure . a2b . b2a . a2b) -- fmap a2b . pure = pure . a2b
   (lmap a2b . pureStar . rmap a2b) (Pure b2a)
-> lmap a2b $ pureStar $ rmap a2b $ Pure b2a
-> lmap a2b $ pureStar $ Pure (a2b . b2a)
-> lmap a2b $ Star (pure . a2b . b2a)
-> Star (pure . a2b . b2a . a2b)

Another example is a dinatural transformation between two Stars using a natural transformation between two functors f and g.

hoistStar :: (forall x. f x -> g x) -> Star f a a -> Star g a a
hoistStar fx2gx (Star a2fa) = Star (fx2gx . a2fa)

You can see hoistStar fx2gx satisfies the dinaturality condition, too.

   (rmap a2b . hoistStar fx2gx . lmap a2b) (Star b2fa)
-> rmap a2b $ hoistStar fx2gx $ lmap a2b $ Star b2fa
-> rmap a2b $ hoistStar fx2gx $ Star (b2fa . a2b)
-> rmap a2b $ Star (fx2gx . b2fa . a2b)
-> Star (fmap a2b . fx2gx . b2fa . a2b)
   (lmap a2b . hoistStar fx2gx . rmap a2b) (Star b2fa)
-> lmap a2b $ hoistStar fx2gx $ rmap a2b $ Star b2fa
-> lmap a2b $ hoistStar fx2gx $ Star (fmap a2b . b2fa)
-> lmap a2b $ Star (fx2gx . fmap a2b . b2fa)
-> Star (fx2gx . fmap a2b . b2fa . a2b)
-> Star (fmap a2b . fx2gx . b2fa . a2b) -- fx2gx . fmap a2b = fmap a2b . fx2gx

Actually, they satisfy the naturality condition as well when you relax their types. So they’re not only dinatural transformations, but natural transformations. Notice that a natural transformation pureStar is indexed by a and b while the previous pureStar was index only by a.

pureStar :: forall f a b. (Applicative f) => Pure a b -> Star f a b
pureStar (Pure a2b) = Star a2fb
  where
    a2fb :: a -> f b
    a2fb = pure . a2b

Let’s expand the naturailty condition for this pureStar to see if it satisfies the naturality conditions in addition to the dinaturality condition.

   (rmap a2b . pureStar . lmap a2b) (Pure b2a)
-> rmap a2b $ pureStar $ lmap a2b $ Pure b2a
-> rmap a2b $ pureStar $ Pure (b2a . a2b)
-> rmap a2b $ Star (pure . b2a . a2b)
-> Star (fmap a2b . pure . b2a . a2b)
-> Star (pure . a2b . b2a . a2b) -- fmap a2b . pure = pure . a2b
   (lmap a2b . pureStar . rmap a2b) (Pure b2a)
-> lmap a2b $ pureStar $ rmap a2b $ Pure b2a
-> lmap a2b $ pureStar $ Pure (a2b . b2a)
-> lmap a2b $ Star (pure . a2b . b2a)
-> Star (pure . a2b . b2a . a2b)
   (rmap a2b . lmap a2b . pureStar) (Pure b2a)
-> rmap a2b $ lmap a2b $ Star (pure . b2a)
-> rmap a2b $ Star (pure . b2a . a2b)
-> Star (fmap a2b . pure . b2a . a2b)
-> Star (pure . a2b . b2a . a2b) -- fmap a2b . pure = pure . a2b
   (lmap a2b . rmap a2b . pureStar) (Pure b2a)
-> lmap a2b $ rmap a2b $ Star (pure . b2a)
-> lmap a2b $ Star (fmap a2b . pure . b2a)
-> Star (fmap a2b . pure . b2a . a2b)
-> Star (pure . a2b . b2a . a2b) -- fmap a2b . pure = pure . a2b

Are there any dinatural transformations which aren’t natural transformations? Yes, for example, there is a dinatural transformation from a constant profunctor to a pure profunctor.

newtype Const r a b = Const r

instance Profunctor (Const r) where
  dimap :: (s -> a) -> (b -> t) -> Const r a b -> Const r s t
  dimap _ _ (Const r) = Const r

constPure is a dinatural transformation from Const r a a to Pure a a.

constPure :: Const r a a -> Pure a a
constPure _ = Pure id

But it’s not a natural transformation. You should be able to get b from a when you want to make it a natural transformation, but it’s not always possible, and cannot satisfy the naturality condition.

constPure :: Const r a b -> Pure a b
constPure _ = Pure ???