Update to Roles

It has become (somewhat) clear that the Roles mechanism as implemented in GHC 7.8 is insufficient. (See examples below.) This page is dedicated to creating a new design for roles that might fix the problems, continuing the discussion started in #9123.

Here, a is the type of the thing stored in the vector, and it is natural to want to coerce a vector of Ints to a vector of Ages. But, GND would not work here, for very similar reasons to the case above -- we won't be able to coerce m Int to m Age, because we don't know enough about m.

The acme-schoenfinkel package

This next example is the one known (not updated) case of type-safe code that existed before GHC 7.8 that does not work with GHC 7.8's roles. The package acme-schoenfinkel-0.1.1 package (by Ertugrul Söylemez) defines

The idea is that a type is in the Rep class if its next parameter is representational. Thus, we would have instances Rep Maybe, Rep [], Rep Either, Rep (Either a), etc. We would not have Rep G, where G is a GADT.

Using this, we can define GND over join thus (continuing the example above):

Open implementation questions

Currently, all coercions (including representational ones) are unboxed (and thus take up exactly 0 bits) in a running program. (We ignore -fdefer-type-errors here.) But, Core has no way of expressing functions in the coercion language, and the co method above essentially desugars to a coercion function. Either we have to add functions to the language of coercions, or we have to keep the coercions generated by Rep instances boxed at runtime, taking of the space of a pointer and potentially an unevaluated thunk.

The ReaderT example defined ReaderT as a newtype. The Rep instance shown is indeed writable by hand, right now. But, if ReaderT were defined as a data type, the Rep instance would be impossible to write, as there are no newtype-unwrapping instances. It seems a new form of axiom would be necessary to implement this trick for data types. This axiom would have to be produced at the data type definition, much like how newtype axioms are produced with newtype definitions.

The Coercible solver is getting somewhat involved already (#9117, #9131). Can this be incorporated cleanly? We surely hope that the solver is sound with respect to the definition of representational coercions in Core. How complete is it? How will this affect completeness? In other words, will adding this extension necessarily mean that there are more types that are provably representationally equal but which GHC is unable to find this proof?

Other issues

There is a weird asymmetry here. If we know (Rep f, Coercible f g, Coercible a b), we can prove Coercible (f a) (g b). We do this by proving Coercible (f a) (f b) and then Coercible (f b) (g b) and using transitivity. But, note that we do not know Rep g! Furthermore, (Rep f, Coercible f g) do not imply Rep g in the presence of role annotations:

newtype MyMaybe a = Mk (Maybe a)
type role MyMaybe nominal

Here, we have Rep Maybe and Coercible Maybe MyMaybe but not Rep MyMaybe. This is all very strange. Of course, we could define an instance Rep MyMaybe, despite the role annotation, by using the newtype-unwrapping instance. But, what does this mean if the author wants to export MyMaybe abstractly?

Consider the StateT newtype:

newtype StateT s m a = StateT (s -> m (a, s))

Its roles are nominal representational nominal. But, if we have Rep m, then the roles could all be representational. For the a parameter, this is just like ReaderT. But, we are stuck with the s parameter, simply because the s parameter comes beforem in the parameter list. There's no way to assert something about m when describing a property of s.

Other possible designs

The design from the ​POPL'11 paper. This design incorporates roles into kinds. It solves the exact problems here, but at great cost: because roles are attached to kinds, we have to choose a types roles in the wrong place. For example, consider the Monad class. Should the parameter m have type */R -> *, requiring all monads to take representational arguments, or should it have type */N ->*, disallowing GND if join is in the Monad class? We're stuck with a different set of problems. And, there is the pervasiveness of this change, which is why we didn't implement it in the first place.

(This is just Richard thinking out loud. It may be gibberish.) What if we generalize roles to be parameterized? To make the definitions well-formed, roles would be attached directly to type constructors (not the parameters), but be a mapping from 1-indexed natural numbers to roles. As an example, ReaderT's role would be [1 |-> R, 2 |-> R, 3 |-> ((2.1 ~ R) => R; N)]. The first two entries just say that parameters r and m have representational roles. The last entry (3 |-> ((2.1 ~ R) => R; N)) says that, if m's first parameter (that is, parameter 2.1, where the . is some sort of indexing operator -- not a decimal point!) is representational, then so is a; otherwise, a is nominal. This defaulting behavior does not cause coherence problems, as long as the roles are listed in order from phantom to nominal -- if GHC can't prove a more permissive role, a more restrictive one is assumed.

To implement this, we would probably need role evidence sloshing around, not unlike coercions. This evidence would be consumed by appropriately beefed up coercion forms (particularly, the TyConAppCo case). It would be produced by role axioms at every data- and newtype definition.

This design seems something like a middle road between the flexibility and modularity (that is, roles and kinds are distinct) that we have now and the completeness offered by the POPL'11 solution.