Let's tweak GHC's RuntimeRep
stuff to try adding information about
strictness. This information could be helpful for avoiding evaluating
thunks that are guaranteed to already be evaluated. Presently,
RuntimeRep
is defined as:
data RuntimeRep
= LiftedRep
| UnliftedRep
| IntRep
| ...
Let's adjust this:
data Strictness = Strict | Lazy
data RuntimeRep
= LiftedRep Strictness
| UnliftedRep
| IntRep
| ...
type Type = TYPE ('LiftedRep 'Lazy)
In Levity Polymorphism,
Eisenberg and Peyton Jones argue that "kinds, not types, are calling
conventions". So what's the calling convention for these two new
kinds? We want LiftedRep Lazy
to behave just like the old LiftedRep
did, and we want LiftedRep Strict
to behave like UnliftedRep
.
So why bother introducing something new rather than reusing UnliftedRep
?
The answer is that we want some cool magic primitives, forget
and seqTyped
. These cannot be used with things like Array#
.
We'll get to them later. Let's write a spine-strict element-lazy
list data type as:
data List :: Type -> forall (s :: Strictness) -> TYPE ('LiftedRep s)
Cons :: a -> List a 'Strict -> List a s
Nil :: List a s
The strictness type variable needs to show up last, and it must have role phantom. That is, the data constructor cannot perform GADT-like analysis on it, and they cannot give it to their fields. With this in mind, we can throw this in the mix:
forget :: forall (a :: forall (s :: Strictness) -> TYPE ('LiftedRep s)). a 'Strict -> a 'Lazy
And forget
just compiles to a no-op. Any evaluated closure can be
treated as an unevaluated closure, a fortunate consequence of how
GHC's runtime was designed. (Notice that users cannot call this
on something like Array#
). Next, let us consider a better seq
:
seqTyped :: forall (a :: forall (s :: Strictness) -> TYPE ('LiftedRep s)). a 'Lazy -> a 'Strict
This function scrutinizes argument. This is not a no-op. It
turns into case
in GHC Core. Speaking of which, that case
would work differently. In GHC Core, we would want case
to
provide make the new identifier have a strict type:
case (ds1 :: List Int Lazy) of (ds2 :: List Int Strict) {
__DEFAULT -> ...
}
You could even have wrappers to support standard non-strict data types. If a user wrote:
data Foo = Foo Bool Bool Bool
The compiler could generate a wrapper:
data FooInternal :: forall (s :: Strictness) -> TYPE ('LiftedRep s) where
FooInternal :: Bool -> Bool -> Bool -> FooInternal s
newtype Foo = Foo (FooInternal 'Lazy)
And the user is none the wiser. Every data type would need to be internally rewritten in this way. Is all of this stuff actually sound? Who knows? It looks sound to me. But you really need someone who can write cool proofs like this:
τ type
---------------------
something → goes here
Ah, that makes sense. And indeed you say so right at the beginning, but I couldn't make much sense of it without reading the proposal first.
But then there's a cringeworthy separation between
Lifted Strict
andUnlifted
. Apart fromLifted Strict
not actually making much sense (but let's not engage in a bike-shedding discussion here), I can imagine that we want to write polymorphic functions that work on both. It sounds to me likeLifted Strict
is really a sub-rep ofUnlifted
, namely those data types which also have a lifted variant.But why not solve this with a type class? I called this
LevityPolymorphic
/LevityCoercible
(seems I wasn't very consistent) on the wiki page.ByteArray#
won't be an instance of this type-class, whereas user-defined data types might be. We could either have a semi-automatic mechanism viaderiving
or have it fully automated likeCoercible
. Then there's no need for a separateUnlifted
anymore.