|
| 1 | +# Roles |
| 2 | + |
| 3 | +Solving [Prim.Coerce.Coercible](https://pursuit.purescript.org/builtins/docs/Prim.Coerce#t:Coercible) constraints requires the compiler to know the _role_ played by type parameters in the runtime representation of their type. There's three roles, from most to least restrictive: _nominal_, _representational_ and _phantom_. |
| 4 | + |
| 5 | +The primitives `->`, `Array` and `Record` types have _representational_ parameters and roles are otherwise inferred based on their appearance in the right hand side of a data type declaration. |
| 6 | + |
| 7 | +## Role inference |
| 8 | + |
| 9 | +* Nominal roles are inferred for parameters of foreign data types, since we do not have enough information to safely choose a less restrictive role. |
| 10 | + |
| 11 | +Nominal parameters are only coercible to themselves: |
| 12 | + |
| 13 | +```purescript |
| 14 | +foreign import data Nominal :: Type -> Type |
| 15 | +``` |
| 16 | + |
| 17 | +`Coercible (Nominal a) (Nominal b)` does not hold, even when `Coercible a b` does. |
| 18 | + |
| 19 | +Albeit a safe default, this is often too constraining. Such coercions can be allowed with a [role annotation](#role-annotations) when it is known to be safe. |
| 20 | + |
| 21 | +Nominal roles are also inferred for constrained parameters: |
| 22 | + |
| 23 | +```purescript |
| 24 | +newtype Shown a = Shown ((Show a => a -> String) -> String) |
| 25 | +``` |
| 26 | + |
| 27 | +`Coercible (Shown a) (Shown b)` does not hold, even when `Coercible a b` does. |
| 28 | + |
| 29 | +Inferring a more permissive role would allow to coerce instances dictionnaries, which would threaten _coherence_: we could exhibit multiple type class instances with different behaviour for the same type. |
| 30 | + |
| 31 | +```purescript |
| 32 | +shown :: forall a. Shown a -> String |
| 33 | +shown (Shown f) = f show |
| 34 | +
|
| 35 | +newtype HTML = MkHTML String |
| 36 | +instance showHTML :: Show HTML where |
| 37 | + show (MkHTML s) = "(HTML " <> show s <> ")" |
| 38 | +
|
| 39 | +shownString :: Shown String |
| 40 | +shownString = Shown (\f -> f "Hello") |
| 41 | +
|
| 42 | +shownHTML :: Shown HTML |
| 43 | +shownHTML = Shown (\f -> f (MkHTML "Hello")) |
| 44 | +
|
| 45 | +badShownHTML :: Shown HTML |
| 46 | +badShownHTML = coerce shownString |
| 47 | +``` |
| 48 | + |
| 49 | +``` |
| 50 | +> :type shownHTML |
| 51 | +Shown HTML |
| 52 | +
|
| 53 | +> shown shownHTML |
| 54 | +"(HTML \"Hello\")" |
| 55 | +
|
| 56 | +> :type badShownHTML |
| 57 | +Shown HTML |
| 58 | +
|
| 59 | +> shown badShownHTML |
| 60 | +"\"Hello\"" |
| 61 | +``` |
| 62 | + |
| 63 | +* Representational roles are inferred for parameters appearing under at least one of the constructors of their type. |
| 64 | + |
| 65 | +Representational parameters are coercible when a Coercible constraint holds. |
| 66 | + |
| 67 | +```purescript |
| 68 | +data Maybe a = Nothing | Just a |
| 69 | +``` |
| 70 | + |
| 71 | +`Coercible (Maybe a) (Maybe b)` holds only when `Coercible a b` does. |
| 72 | + |
| 73 | +This rule must be amended for parameters appearing in the arguments of a type variable. Because the variable could be instantiated to anything we have to be conservative and infer _nominal_, the most restrictive role, instead of _representational_: `Coercible (a -> f b) (a -> f c)` does not hold, even when `Coercible b c` does. |
| 74 | + |
| 75 | +* Phantom roles are inferred for parameters not appearing at all under any constructor of their type. |
| 76 | + |
| 77 | +Phantom parameters are coercible to anything: |
| 78 | + |
| 79 | +```purescript |
| 80 | +data Proxy a = Proxy |
| 81 | +``` |
| 82 | + |
| 83 | +`Coercible (Proxy a) (Proxy b)` holds for all `a` and `b`, regardless of `Coercible a b`. |
| 84 | + |
| 85 | +* Roles of parameters appearing in the arguments of another type are inferred from the declaration of that type: |
| 86 | + |
| 87 | +```purescript |
| 88 | +newtype First a = First (Maybe a) |
| 89 | +``` |
| 90 | + |
| 91 | +`Coercible (First a) (First b)` holds when `Coercible a b` does, even when the newtype constructor is out of scope, because the parameter of `Maybe` is _representational_. |
| 92 | + |
| 93 | +We cannot infer the role of parameters in recursive position this way, so we default to _phantom_ but usually end up with something else because we keep the most restrictive role a parameter appears at: |
| 94 | + |
| 95 | +```purescript |
| 96 | +data List a = Nil | Cons a (List a) |
| 97 | +``` |
| 98 | + |
| 99 | +Here the parameter appears at _representational_ (under the `Cons` constructor) and _phantom_ (in recursive position) roles so we infer _representational_: `Coercible (List a) (List b)` holds when `Coercible a b` does. |
| 100 | + |
| 101 | +```purescript |
| 102 | +newtype Mu f = In (f (Mu f)) |
| 103 | +``` |
| 104 | + |
| 105 | +Here the parameter appears at _representational_ (under the `In` constructor) and _nominal_ (as argument to itself) roles so we infer _nominal_: `Coercible (Mu f) (Mu g)` does not hold, unless `g` is actually `f`. |
| 106 | + |
| 107 | +## Role annotations |
| 108 | + |
| 109 | +Inferring _nominal_ roles for foreign data types is safe but can be too constraining sometimes. For example this prevents to coerce `Effect Age` to `Effect Int`, even though they actually have the same runtime representation. |
| 110 | + |
| 111 | +The roles of foreign data types can thus be loosened with explicit role annotations, similar to the [RoleAnnotations](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#role-annotations) GHC extension. |
| 112 | + |
| 113 | +Conversely, we might want to strengthen the roles of parameters with invariants invisible to the type system. Maps are the canonical example of this: the shape of their underlying tree rely on the `Ord` instance of their keys, but the `Ord` instance of a newtype may behave differently than the one of the wrapped type so it would be unsafe to allow coercions between `Map k1 a` and `Map k2 a`, even when `Coercible k1 k2` holds. |
| 114 | + |
| 115 | +A role annotation starts with `type role`, then the name of the annotated type and the role (`nominal`, `representational` or `phantom`) of each parameters of the type: |
| 116 | + |
| 117 | +```purescript |
| 118 | +type role Effect representational |
| 119 | +type role Map nominal representational |
| 120 | +``` |
| 121 | + |
| 122 | +Role annotations are only allowed for data and newtype declarations. They have to immediately follow the annotated type declaration. |
| 123 | + |
| 124 | +Annotated roles are compared against the roles inferred by the compiler so it is not possible to compromise safety by ascribing too permissive roles, except for foreign types. |
0 commit comments