7

I have two types (<->) and (<-->) representing isomorphisms between types:

data Iso (m :: k -> k -> *) a b = Iso { to :: m a b, from :: m b a }
type (<->) = Iso (->)
infix 0 <->

data (<-->) a b = Iso' { to' :: a -> b, from' :: b -> a }
infix 0 <-->

The only difference between the two is that (<->) is a specialization of a more general type.

I can coerce (<-->) isomorphisms easily:

coerceIso' :: (Coercible a a', Coercible b b') => (a <--> b) -> (a' <--> b')
coerceIso' = coerce 

But I get an error when I try the same with (<->) isomorphisms:

coerceIso :: (Coercible a a', Coercible b b') => (a <-> b) -> (a' <-> b')
coerceIso = coerce
{-
src/Data/Iso.hs:27:13: error:
    • Couldn't match type ‘a’ with ‘a'’ arising from a use of ‘coerce’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          coerceIso :: forall a a' b b'.
                       (Coercible a a', Coercible b b') =>
                       (a <-> b) -> a' <-> b'
        at src/Data/Iso.hs:25:1-73
      ‘a'’ is a rigid type variable bound by
        the type signature for:
          coerceIso :: forall a a' b b'.
                       (Coercible a a', Coercible b b') =>
                       (a <-> b) -> a' <-> b'
        at src/Data/Iso.hs:25:1-73

-}

My current work-around is to coerce the forwards and backwards functions separately:

coerceIso :: (Coercible a a', Coercible b b') => (a <-> b) -> (a' <-> b')
coerceIso (Iso f f') = Iso (coerce f) (coerce f')

But why is such a workaround is necessary? Why can't (<->) be coerced directly?

rampion
  • 87,131
  • 49
  • 199
  • 315
  • 3
    Ok, I think I've got it. There's an implicit `type role Iso representational nominal nominal` since there's no way for the compiler to predict whether `m`'s parameters are nominal or representational, so it plays it safe. Now I just wish there was a way I could require `type role m representational representational` – rampion Jun 27 '19 at 15:06
  • 4
    It will be possible to specify such type roles after this GHC proposal is implemented: https://github.com/ghc-proposals/ghc-proposals/pull/233 I've run into a similar problem just yesterday. – Shersh Jun 27 '19 at 15:19

2 Answers2

8

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.

chi
  • 111,837
  • 3
  • 133
  • 218
  • 1
    Yeah, I was expecting some sort of dependent type roles where `type role Iso m = type role m` – rampion Jun 27 '19 at 15:08
  • 1
    @rampion Indeed. I tried to attach the `type role` to the synonym but that's also disallowed. – chi Jun 27 '19 at 15:10
2

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.

luqui
  • 59,485
  • 12
  • 145
  • 204
  • Nice! Doesn’t work in my current case as [`m` is a natural transformation](https://github.com/rampion/concrete-kinds/blob/8e1285fa8f30d1db50e2b656ab8d731a1f3a9d20/src/Kind/Concrete/Natural.hs#L35) but definitely a useful trick to put in my toolbox. – rampion Jun 28 '19 at 20:26