Revise Levity Polymorphism

GHC 8 introduced levity polymorphism, where a variable can be polymorphic in its representation. (This is also known as “representation polymorphism”, but levity polymorphism surely has a better ring to it. This proposal also solidifies this name for the feature.) However, levity polymorphism still has some rough edges; this proposal smoothes them out by parameterizing UnboxedTupleRep and UnboxedSumRep. This proposal also renames some definitions to make them shorter.

Motivation

This is useful in the types of undefined and error as well as in simplifying type inference a good deal, with the abolishment of sub-kinding.

As @Iceland_jack has pointed out, levity polymorphism also allows us to overload operations like + over unlifted types. (See this bug report.)

Further motivation is available in Simon and my paper on the subject.

Motivating this change is the fact that unboxed tuples and sums don’t really work. For example, GHC accepts this nonsense:

type family F a where
  F Int  = (# Int, Bool #)
  F Bool = (# Int# #)

It’s nonsense because the two return types actually have different representations! With the changes proposed here, such a type family would be safe.

Proposed Change

Here is the current state of affairs:

TYPE :: RuntimeRep -> Type   -- super-magical constant
data RuntimeRep = PtrRepLifted
                | PtrRepUnlifted
                | VoidRep
                | IntRep
                | FloatRep
                | ...
                | UnboxedTupleRep
                | UnboxedSumRep
type Type = TYPE PtrRepLifted

With these definitions, all “normal” types have type TYPE PtrRepLifted (that is, Type). Unboxed types are kinded similarly, with Int# :: TYPE IntRep and Array# :: Type -> TYPE PtrRepUnlifted.

The problem is that all unboxed tuples have the same kind, TYPE UnboxedTupleRep. This violates the intent of levity polymorphism, wherein the kind of a type tells you its representation (and calling convention).

I propose changing this to become:

TYPE :: RuntimeRep -> Type   -- same super-magical constant
data RuntimeRep = LiftedRep
                | UnliftedRep
                | IntRep
                | FloatRep
                | ...
                | TupleRep [RuntimeRep]
                | SumRep [RuntimeRep]
type Type = TYPE LiftedRep

Note the name changes and the new parameters to TupleRep and SumRep. These parameters mean that different unboxed tuples/sums have different kinds. Hooray!

Also, this change removes VoidRep in favor of TupleRep [], a small simplification.

Drawbacks

The renaming is indeed gratuitous. But it still seems early enough in the adoption of this feature that we can do this. Note that the changed names are exported only by GHC.Types and GHC.Exts, so programs using these names have opted into experimental features.

Alternatives

I argue that the status quo is untenable, because it fails to fulfill the promise of levity polymorphism.

One alternative is presented in Simon and my paper on the subject, where TYPE :: [RuntimeRep] -> Type (note the list!). The list contains the representations of all components of an unboxed tuple. Anything other than an unboxed tuple has a singleton list. This alternative is more elaborate than what is proposed here, and it would allow, for example

foo :: forall (a :: TYPE '[IntRep, FloatRep]). a -> a
foo x = x

to be instantiated, say, at both (# Int#, Float# #) and (# Int#, (# (# #), Float# #) #), because these both have the same representation. This is all well and type-safe, but no one is really asking for this feature, and it complicates the type system.

A fully broken partial implementation of this is available here.

Unresolved Questions

I don’t have any at the moment.