Type Annotated Quoters

The existing QuasiQuoter type is capable of producing expressions, but cannot describe the types of those expressions. We propose adding a TQuasiQuoter a type capable of producing expressions of type a.

Motivation

QuasiQuoter is a powerful tool to do compilation time code construction, but the type QuasiQuoter is rather uninformative — especially when it’s used exclusively as an expression splice. QuasiQuoters which appear in haddock documentation don’t describe their resulting types, requiring a convention of comments. This is clearly against the spirit of a language with a typesystem as strong as Haskell’s.

Furthermore, by exposing the type of the resulting expression, a typed quasiquoter can participate in typechecking.

Proposed Change Specification

We will introduce a new datatype in Language.Haskell.TH.Quote:

data TQuasiQuoter a = TQuasiQuoter
  { quoteTExp :: String -> Q (TExp a)
  }

and will add new syntactic sugar when -XTemplateHaskell is enabled:

[someTQQ|| hello world ||]

whose desugaring is:

$$(quoteTExp someTQQ "hello world")

This new typed quasiquoter syntax is identical to untyped quasiquoters, except that it requires double bars. All of the usual rules around quasiquoted strings apply here.

Typed quasiquoters participate in typechecking and unification. We can put given constraints on a value of TQuasiQuoter, and they will be available as usual in its definition. For example, the following is allowed:

str :: IsString a => TQuasiQuoter a
str = TQuasiQuoter $ \a -> [|| fromString a ||]

as is its concrete use:

someText :: Text
someText = [str|| hello world ||]

and polymorphic use:

something :: IsString a => a
something = [str|| hello world ||]

Examples

We can use a typed quasiquoter to implement safe custom Numeric types, which can’t be overflowed at compile-time. While -Woverflowed-literals can help with built-in types, it won’t help for custom types!

checkedNum
    :: forall a
     . (Bounded a, Num a, Integral a, Typeable a)
    => TQuasiQuoter a
checkedNum = TQuasiQuoter $ \str ->
  let minVal = fromIntegral $ minBound @a
      maxVal = fromIntegral $ maxBound @a
      val = read @Integer str
   in if minVal <= val && val <= maxVal
      then [|| fromInteger val ||]
      else fail $ mconcat
        [ show val
        , " is out of bounds for "
        , show (typeRep $ Proxy @a)
        ]

Additionally, @yav gives an example in which we parse an AST from a string, and then separately compile that down to a TExp:

data Expr = Fun String Expr | Add Expr Expr | Var String
            deriving Show

type Code a = Q (TExp a)

-- Language quoter
lam :: TQuasiQuoter Expr
lam = TQuasiQuoter $ \input ->
  case pExpr (words input) of
    Just(e,[]) -> e
    _          -> fail "Parse error"

pExpr :: [String] -> Maybe (Code Expr, [String])
pExpr s = case s of
            "ADD" : s1 ->
               do (a,s2) <- pExpr s1
                  (b,s3) <- pExpr s2
                  pure ([|| Add $$a $$b ||], s3)

            "FUN" : v : "->" : s1 ->
               do (a,s2) <- pExpr s1
                  pure ([|| Fun v $$a ||] , s2)

            x : s1 ->
              pure ([|| Var x ||], s1)

data Val = VFun (Val -> Val) | VInt Int | VErr

compile :: [(String,Code Val)] -> Expr -> Code Val
compile env expr =
  case expr of
    Fun x e -> [|| VFun (\i -> $$(compile ((x, [|| i ||]) : env) e)) ||]

    Var x -> case lookup x env of
               Just i -> i
               _      -> [|| VErr ||]

    Add x y -> [|| case ($$(compile env x), $$(compile env y)) of
                    (VInt x, VInt y) -> VInt (x + y)
                    _ -> VErr ||]

Effect and Interactions

For expression quoter writers, adding TQuasiQuoter a mainly reduce the documentation burden since the result expression’s type is already annotated. Users can spot the result type much more easily and become more confident in using these quoters. When beginners click through the TQuasiQuoter document link, they’re supposed to get the basic knowledge on how to enable some language extensions and splice quoters into their code.

simonpj raises another point, that this proposal will improve error messages, consider:

qq :: TQuasiQuoter Char
qq = ...

blah = [qq|| unicode 78 ||] && True

With existing quasi-quote machinery we’d first have to run qq, splice in the resulting syntax tree, and then complain if it didn’t typecheck. With a typed quasi-quoter we can complain right away: qq returns a TExp Char and that doesn’t fit somewhere a Bool is needed.

Finally, this proposal finishes the syllogism that Exp : QuasiQuoter :: TExp : ?.

Alternatives

In fact this proposal is inspired by the Compile-time literal values proposal, and shared some goals, but this proposal is more about trying to solve an existing issue with current quoters.

Implementation Plan

Matthew Pickering has graciously offered to implement this, and sighingnow was nominated by the original author of this proposal.