Make Q (TExp a) into a newtype¶
I propose to modify the Typed Template Haskell API to make the Code
type
more abstract. In particular we introduce a new data type called Code
which
is abstract such that Code m a
represents an expression of type a
produced
in a monadic context m
. The reader should note that this proposal builds on
top of the overloaded quotations proposal which was
accepted before this proposal.
The Untyped Template Haskell API is unmodified. This proposal is only about Typed Template Haskell.
Motivation¶
There are three problems with the current API:
It is hard to properly write instances for
Quote m => m (TExp a)
as the type is the composition of two type constructors. Doing so in your program involves making your own newtype and doing a lot of wrapping/unwrapping.For example, if I want to create a language which I can either run immediately or generate code from I could write the following with the new API.
class Lang r where _int :: Int -> r Int _if :: r Bool -> r a -> r a -> r a instance Lang Identity where _int = Identity _if (Identity b) (Identity t) (Identity f) = Identity (if b then t else f) instance Quote m => Lang (Code m) where _int = liftTyped _if cb ct cf = [|| if $$cb then $$ct else $$cf ||]
When doing code generation it is common to want to store code fragments in a map. When doing typed code generation, these code fragments contain a type index so it is desirable to store them in one of the parameterised map data types such as
DMap
fromdependent-map
orMapF
fromparameterized-utils
.compiler :: Env -> AST a -> Code Q a data AST a where ... data Ident a = ... type Env = MapF Ident (Code Q) newtype Code m a = Code (m (TExp a))
In this example, the
MapF
maps anIdent String
directly to aCode Q String
. Using one of these map types currently requires creating your own newtype and constantly wrapping every quotation and unwrapping it when using a splice. Achievable, but it creates even more syntactic noise than normal metaprogramming.m (TExp a)
is ugly to read and write, understandingCode m a
is easier. This is a weak reason but one everyone can surely agree with.
Proposed Change Specification¶
In this section, the proposed complete interface for Code
and TExp
is
given.
A newtype is defined called Code
:
newtype Code m a = Code (m (TExp a))
There are three main constructs that the proposal affects.
Quoting an expression e :: T
now produces an expression of typed Quote m => Code m T
:
-- foo :: Quote m => m (TExp Int)
foo :: Quote m => Code m Int
foo = [|| 5 ||]
Top-level splicing requires an expression of type Code Q T
and produces a value of type T
:
bar :: Int
bar = $$foo
Nested splicing requires an expression of type Code m T
and the overall
type of the quotation is a union of the constraints on all the nested splices:
baz :: Quote m => Code m Int
baz = [|| 1 + $$(foo) ||]
The return type of the liftTyped
method of the class Lift
is changed from m (TExp a)
to Code m a
.:
class Lift a where
lift :: Quote m => a -> m Exp
liftTyped :: Quote m => a -> Code m a
The functions unsafeCodeCoerce
and unTypeCode
are introduced to work directly
with Code
:
unsafeCodeCoerce :: m Exp -> Code m a
unTypeCode :: Code m a -> m Exp
There are still the normal functions for interacting with TExp a
:
unsafeTExpCoerce :: Quote m => m Exp -> m (TExp a)
unsafeTExpCoerce = fmap unsafeExpToTExp
TExp :: Exp -> TExp a
unType :: TExp a -> Exp
unType (TExp a) = a
A new function is added to Language.Haskell.TH.Syntax
in order to perform monadic actions inside of Code
:
liftCode :: m (TExp a) -> Code m a
liftCode = Code
And also a function which allows access to the wrapped TExp
value:
examineCode :: Code m a -> m (TExp a)
examineCode (Code m) = m
Code
is still exported though so users can pattern match on it themselves
rather than using these convenience functions.
It is also useful to implement a method to modifying the underlying monadic representation. For example, in order to handle additional effects before running a top-level splice:
hoistCode :: (forall a . m a -> n a) -> Code m a -> Code n a
hoistCode f (Code a) = Code (f a)
-- As an example, hoistCode can be used to handle a state effect
handleState :: Code (StateT Int Q) a -> Code Q a
handleState = hoistCode (flip runState 0)
Two more useful combinators are bindCode
and bindCode_
which
are versions of >>=
and >>
and interact nicely with QualifiedDo:
bindCode :: m a -> (a -> Code m b) -> Code m b
bindCode q k = liftCode (q >>= examineCode . k)
bindCode_ :: m a -> Code m b -> Code m b
bindCode_ q c = liftCode (q >> examineCode c)
The Code
data constructor is also exposed to users in case they want to
explicitly interact with the underlying monadic computation in another manner.
Effect and Interactions¶
The proposal solves the main problem because now it is easily possible to write
instances for the Code
type because it is no longer a composition of two
type constructors.
Costs and Drawbacks¶
The main drawback is that this will break all users of Typed Template Haskell who write type signatures. However, I feel like I am the only user so the impact will be minimal.
Alternatives¶
Unresolved Questions¶
Implementation Plan¶
Implementation is straightforward.