Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problem coercing a record with complex type parameters

I have this record:

import Data.Functor.Identity
import Control.Monad.Trans.Identity
import Data.Coerce

data Env m = Env {
        logger :: String -> m ()
    }

env :: Env IO
env = undefined

and this coercion function

decorate 
    :: Coercible (r_ m) (r_ (IdentityT m))   
    => r_ m -> r_ (IdentityT m)        
decorate = coerce

which is applicable to the record value without problems:

decoratedEnv :: Env (IdentityT IO)
decoratedEnv = decorate env

However, if I define the only slightly more complex record

data Env' h m = Env' {
        logger' :: h (String -> m ())
    }

env' :: Env' Identity IO
env' = undefined

And try to insert an IdentityT wrapper like I did before

decoratedEnv' :: Env' Identity (IdentityT IO)
decoratedEnv' = decorate env'

I get the error:

* Couldn't match type `IO' with `IdentityT IO'
    arising from a use of `decorate'

To my mind, the extra Identity parameter taken by Env' shouldn't stop coerce from working. Why does coerce fail in this case? Is there a way to make coerce work?

like image 466
danidiaz Avatar asked Nov 09 '21 14:11

danidiaz


1 Answers

A coercion Coercible A B does not imply a coercion Coercion (h A) (h B) for all h.

Consider this GADT:

data T a where
  K1 :: Int  -> T A
  K2 :: Bool -> T B

Coercing T A to T B amounts to coercing Int to Bool, and that clearly should never happen.

We could perform that coercion only if we knew that the parameter of h has the right role (e.g. representational or phantom, but not nominal).

Now, in your specific case h is known (Identity), and has surely the right role, so it should work. I guess the GHC coercion system is not so powerful to handle such a "well behaved" h while rejecting the ill-behaved ones, so it conservatively rejects everything.


Adding a type hole seems to confirm this. I tried

decoratedEnv' :: Env' Identity (IdentityT IO)
decoratedEnv' = _ decorate' @(Env' Identity)

and got the error

* Couldn't match representation of type: r_0 m0
                           with that of: r_0 (IdentityT m0)
    arising from a use of decorate'
  NB: We cannot know what roles the parameters to `r_0' have;
    we must assume that the role is nominal
* In the first argument of `_', namely decorate'
  In the expression: _ decorate' @(Env' Identity)
  In an equation for decoratedEnv':
      decoratedEnv' = _ decorate' @(Env' Identity)

The "NB:" part is right on the spot.

I also tried to insist, and force the role

type role Env' representational representational
data Env' h m = Env' {
        logger' :: h (String -> m ())
    }

to no avail:

* Role mismatch on variable m:
    Annotation says representational but role nominal is required
* while checking a role annotation for Env'

The best workaround I could find is to unwrap/rewrap, and exploit QuantifiedConstraints to essentially require that h has a non-nominal role:

decorate' 
    :: (forall a b. Coercible a b => Coercible (h a) (h' b))
    => Coercible m m'
    => Env' h m -> Env' h' m'
decorate' (Env' x) = Env' (coerce x)

decoratedEnv' :: Env' Identity (IdentityT IO)
decoratedEnv' = decorate' env'

Not an ideal solution, since it goes against the spirit of Coercible. In this case, the rewrapping has only O(1) cost, but if we had, say, a list of Env' Identity IO to convert, we would pay O(N).

like image 57
chi Avatar answered Oct 16 '22 20:10

chi