Skip to content

Commit 57952c5

Browse files
authored
Document roles, role inference and role annotations (#371)
* Document roles, role inference and role annotations * fixup! Document roles, role inference and role annotations
1 parent ec9d39d commit 57952c5

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

language/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ The full language reference continues below:
4141
5. [Modules](Modules.md)
4242
6. [FFI](FFI.md)
4343
7. [Records](Records.md)
44-
8. [Differences from Haskell](Differences-from-Haskell.md)
44+
8. [Roles](Roles.md)
45+
9. [Differences from Haskell](Differences-from-Haskell.md)

language/Roles.md

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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

Comments
 (0)