Motiviation
GHC Haskell has a nice runtime representation system, centered around RuntimeRep and TYPE :: RuntimeRep -> * in GHC.Exts, that enables representation-polymorphic abstractions. Unfortunately, this control over representations came late in Haskell’s life, so most of the existing abstractions in the ecosystem (e.g., almost all of the standard type classes in base) work only over TYPE LiftedRep (i.e., *). I’m interested in creating an alternative foundational library (most likely largely incompatible with the existing ecosystem) to reduce the pain of using unlifted types, for two reasons:
- To achieve better performance (e.g., avoiding allocation with unboxed types).
- To program with strict evaluation (controversial, I know). (Why not just use strictness annotations (or
Strict{,Data})? Because, unlike switching fromTYPE LiftedReptoTYPE UnliftedRep, you can’t apply strictness annotations to categorically rule-out bottoms fordatatypes, so certain laws on standard type classes don’t hold. E.g., that’s whyData.Strict.Maybefrom the strict package isn’t an applicative functor; ifApplicativesomehow supported functors with domainTYPE UnliftedRep, and ifData.Strict.Maybeabstracted overTYPE UnliftedRep(instead of overTYPE LiftedRepwith a strict annotation on theJustconstructor), then we could haveApplicative Data.Strict.Maybe.)
Problem
There are a number of obstacles to creating representation-polymorphic abstractions in Haskell, but the one I want to focus on here is that representations must be “concrete” in certain circumstances (details in Downen et al., “Kinds are Calling Conventions”). I think the main two restrictions are as follows:
- You can’t form a lambda whose parameter has non-concrete representation. E.g., this doesn’t compile:
foo :: forall (r :: RuntimeRep) (a :: TYPE r). a -> Int foo _ = 0 - You can’t form an application whose argument has non-concrete representation. E.g., this doesn’t compile:
bar :: forall (r :: RuntimeRep) (a :: TYPE r) b. (a -> b) -> (() -> a) -> b bar f g = f (g ())
Solution Idea
Inspired by Edward Kmett’s unboxed experiment and Conal Elliott’s work on “Compiling to Categories”, I’m trying to get around the concreteness restrictions on representation polymorphism with RuntimeRep-indexed type classes.
For example, in conjunction with DefaultSignatures, Edward applies this idea to define representation-polymorphic type classes with default method implementations. Look at how his version of Eq utilizes an extra RuntimeRep-indexed class EqRep: the naive default implementation of (/=) as a /= b = not (a == b) does not satisfy the concreteness constraint on lambdas, because a and b have non-concrete representation.
The downside of Edward’s approach is that we must implement each RuntimeRep-indexed class (EqRep, etc.) for each desired RuntimeRep in advance. Since there are infinitely-many RuntimeReps (thanks to the recursive TupleRep and SumRep constructors), this is a losing battle. (Of course, you can automate the instance generation, as Edward does for unboxed tuples and sums of arity 2. See the cabal file.)
It would be much nicer if the compiler would automatically generate the required instances of RuntimeRep-indexed classes on demand. So we would, for instance, (1) write Eq and EqRep exactly as Edward has, (2) write a RuntimeRep-uniform implementation “schema” for EqRep once, then (3) rely on the compiler to synthesize an instance of EqRep from the schema whenever EqRep r arises as a constraint with concrete r :: RuntimeRep.
I think this is possible with a GHC type checker plugin. I’m not very familiar with GHC internals in general, but I think it could work something like this:
-
User POV:
- The user writes a type class
MyClassand marks it as “RuntimeRep-monomorphizable” with an annotation (ANNpragma). (If it helps, we could restrict the form of these type classes somewhat; e.g.,class MyClass (r :: RuntimeRep) where ...probably covers most cases.) - The user writes an implementation schema for each
MyClassmethod. I’m not sure how this should work; it’s probably the hard part. - The user freely uses
MyClass rconstraints.
- The user writes a type class
-
Plugin POV:
- The plugin looks for classes annotated as
RuntimeRep-monomorphizable and findsMyClass(in this example, anyway; in general, there can be many). - Each time GHC can’t itself solve a
MyClass rconstraint, it consults the plugin. Ifris a concrete representation, the plugin synthesizes a instance ofMyClass rand returns it as evidence. (Hopefully, GHC only consults type checker plugins for constraints it can’t solve itself; otherwise, the plugin has to check if a suitable instance already exists.) (Ifris not a concrete representation, the plugin does nothing.)
- The plugin looks for classes annotated as
Request for Comments/Hints
Is this all a terrible idea? Is there a better way to do it?
If the suggested solution isn’t too bad, any hints (existing plugins to study, relevant parts of the GHC API to apply, etc.) would be greatly appreciated.