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
I'm concerned about the precedent for putting erasable stuff in the runtime rep. Per https://gitlab.haskell.org/ghc/ghc/issues/14917 it would be nice to have a monomorphizing quantifier. But with your plan we are left with a requirement that the runtime rep need only be shallowly monomorphized:
Strict | Lazy
is behind the pointer and thus not relevent. This means the "const eval" system that would go with the memorizing compiler would have to track not just all (deep) or nothing constantness, but also shallow (head) constantness.condensed into one example, I'm worried about how to allow: