Why do I have to coerce this data type by fields, rather than all at once?

The problem lies in the roles of the arguments m in your general Iso type.

Consider:

data T a b where
  K1 :: Int    -> T () ()
  K2 :: String -> T () (Identity ())

type (<->) = Iso T

You can't really expect to be able to convert T () () into T () (Identity ()) even if () and Identity () are coercible.

You would need something like (pseudo code):

type role m representational representational =>
          (Iso m) representational representational

but this can not be done in current Haskell, I believe.


Not a direct answer, but I want to share this relevant trick: Whenever m is a profunctor (I suspect it will usually be), you can use a Yoneda-esque transformation to make an equivalent type with representational arguments.

newtype ProYo m a b = Yo2 (forall x y. (x -> a) -> (b -> y) -> m x y)

ProYo m is isomorphic to m, except its argument roles are representational, by the following isomorphism:

toProYo :: (Profunctor m) => m a b -> ProYo m a b
toProYo m = ProYo (\f g -> dimap f g m)

fromProYo :: ProYo m a b -> m a b
fromProYo (ProYo p) = p id id

If we define your Iso in terms of this

data Iso m a b = Iso { to :: ProYo m a b, from :: ProYo m b a }

coerceIso passes without modification.