I have a type Id a and I'm trying to prevent accidentally coercing, e.g., an Id Double to an Id Int.
If I understand type roles correctly, the following should not compile.
{-# LANGUAGE RoleAnnotations #-}
import Data.Coerce (coerce)
type role Id nominal
newtype Id a = Id String
badKey :: Id Int
badKey = coerce (Id "I point to a Double" :: Id Double)
Unfortunately, it does:
Prelude> :load Id.hs
[1 of 1] Compiling Main ( Id.hs, interpreted )
Ok, one module loaded.
*Main> :type badKey
badKey :: Id Int
What am I missing about type roles?
Coerciblehas three possible "types" of instances (which are automagically generated by the compiler, not defined by the user). Only one of them is actually affected by roles.representationalorphantom. For example, you can coerce aMap Char Intinto aMap Char (Data.Monoid.Sum Int)because forMapwe havetype role Map nominal representational.In your example, the third rule applies. Had the newtype been defined in another module and the constructor not imported, the coercion would have failed (to make it work again, you would need to switch the role to
phantom).The somewhat surprising special behaviour for newtypes is explained in this GHC issue.