Levity-polymorphic Lift¶
The Lift
typeclass lets us lift values into typed or untyped Template Haskell expressions.
class Lift t where
lift :: t -> Q Exp
liftTyped :: t -> Q (TExp t)
newtype TExp a = TExp { unType :: Exp }
One shortcoming of this class is that it does not let us lift values whose kind is not Type
.
We propose to lift this restriction by making Lift
and TExp
levity polymorphic.
class Lift (t :: TYPE r) where
lift :: t -> Q Exp
liftTyped :: t -> Q (TExp t)
newtype TExp (t :: TYPE r) = TExp { unType :: Exp }
Motivation¶
The Lift
class cannot currently handle unlifted types. This is a somewhat frustrating limitation
since Template Haskell supports representing unlifted values, only they must be constructed
manually.
x :: Q Exp
x = LitE (IntPrimL 1) -- yet we'd like to be able to write `lift 1#`
The fact that TExp
doesn’t support unlifted types means that it is less expressive that Exp
.
TExp
is generally regarded as a more type-safe approach to Template Haskell, so users find
themselves needing to choose between generating more performant code that uses unlifted types and
using the more typesafe TExp
. The problems around TExp
don’t end there: if you omit a type
signature, one can trick GHC 8.6.3 (and latest HEAD) into accepting invalid TExp
!
{-# LANGUAGE MagicHash, TemplateHaskell #-}
import GHC.Exts
import Language.Haskell.TH.Syntax
main = print =<< runQ (unTypeQ [|| I# $$e ||])
where e = [|| 1# ||] -- :: Q (TExp Int#)
-- yet but adding this ^ signature causes a compile error!
The current -XDeriveLift
implementation has some special logic for handling data types with unlifted
fields. Generalizing Lift
would remove the need for any special cases.
Proposed Change Specification¶
Change TExp
, Lift
, unTypeQ
, and unsafeTExpCoerce
to take advantage of levity polymorphism:
newtype TExp (t :: TYPE r) = TExp { unType :: Exp }
unTypeQ :: forall (a :: TYPE r). Q (TExp a) -> Q Exp
unTypeQ m = do { TExp e <- m
; return e }
unsafeTExpCoerce :: forall (a :: TYPE r). Q Exp -> Q (TExp a)
unsafeTExpCoerce m = do { e <- m
; return (TExp e) }
class Lift (t :: TYPE r) where
lift :: t -> Q Exp
default lift :: (r ~ 'LiftedRep) => t -> Q Exp
lift = unTypeQ . liftTyped
liftTyped :: t -> Q (TExp t)
Two important observations:
The
r ~ 'LiftedRep
is needed because GHC doesn’t know how to handle levity polymorphic binders. It does mean that instances ofLift
over unlifted types will need to manually implement bothlift
andliftTyped
.By not providing a default implementation of
liftTyped
, any existing empty instances ofLift
will crash as opposed to loop. This does have the unpleasant side effect that there will be no way to write a CPP-free and backwards-compatible manual instance ofLift
. However, since the vast majority of instances can (and should!) be derived with-XDeriveLift
, which is backwards compatible, this is not a significant problem.
Add to Language.Haskell.TH.Syntax
a handful of new instances of Lift
which are now valid:
instance Lift Int# where ...
instance Lift Word# where ...
instance Lift Float# where ...
instance Lift Double# where ...
instance Lift Char# where ...
instance Lift Addr# where ...
Modify -XDeriveLift
to generate typed and untyped quotes (for liftTyped
and lift
respectively).
Effect and Interactions¶
The proposed change would make it possible to lift values of unlifted kinds and would make it
(officially) possibly for TExp
to represent expressions of unlifted kinds.
This proposal would also provide a means for trying out levity polymorphism in typeclasses (see
sections 7.3 and 8.1 of the Levity Polymorphism paper).
Although there are many classes which could benefit from this treatment,
Lift
is a good first candidate because it isn’t too widely used and template-haskell
isn’t expected to be as stable and free of breakage as base
. There is already a proposal which aims to make typeclasses in base
levity-polymorphic.
Costs and Drawbacks¶
The main drawback is that uses of lift
, TExp
, unType
, unTypeQ
, and unsafeTExpCoerce
which
have visible type applications will be broken (since the type argument would now refer to the
runtime rep). Here is an example:
ghci> let rationalOne = lift @Rational 1 -- no longer works
<interactive>: error:
• Expected kind ‘GHC.Types.RuntimeRep’, but ‘Rational’ has kind ‘*’
• In the type ‘Rational’
In the expression: lift @Rational 1
In an equation for ‘it’: it = lift @Rational 1
Any such uses should be easily fixable in a backwards compatible fashion by using explicit type
signatures instead of type applications (or, if one wishes to continue using type applications, by
adding an extra @_
type argument first).
In terms of development and maintainability, the cost is small: the prototype patch that implements this functionality ends up removing more lines from the compiler than it adds.
The only other drawback is that beginners browsing the documentation for Template Haskell are more
likely to be confused by the complex signature for the default implementations of the lift
method.
Alternatives¶
None known.
Unresolved Questions¶
If the proposal Explicit specificity in type variable binders
were already implemented, we might be able to avoid breaking code which uses visible type
application (on any of the functions whose signatures we are proposing to generalize). The idea
would be to specify the RuntimeRep
type variables as inferred. This would come at the cost of
even more complicated type signatures though.
Implementation Plan¶
I (Alec Theriault) will implement this change. The work is already done in Phab:D5220.