A Short Overview of Typed Template Haskell

Welcome to our second post on Template Haskell!

Today we will take a quick look at typed Template Haskell. This article assumes some familiarity with Template Haskell (TH) already. If this is your first journey with TH, then check out our introduction to Template Haskell first.

For this article, we will be using GHC 8.10.4. However, we will also show the changes that are necessary so the code works in GHC 9.

Why typed TH?

Typed TH, as the name implies, allows us to provide stronger, static guarantees about the correctness of the meta-program. With untyped TH, the generated expressions would be type-checked when they are spliced, i.e., during their usage, rather than their definition. With typed TH, those expressions are now type-checked at their definition site.

Like with anything else in computer science, there are advantages and disadvantages in using typed TH in comparison to ordinary TH, some of which are listed below.

Advantages:

  • Greater type safety guarantees.
  • Errors won’t be delayed until use; instead, they are reported on their definition.

Disadvantages:

  • Must be used with the [|| ||] quoter.
    • This means that we can’t easily use Exp constructors directly.
    • In comparison, for the untyped quoter, we could either use [| |] or directly call the Exp constructors.
    • Alternatively, you may use unsafeCodeCoerce to work around this, if you’re willing to use unsafe functions.
  • Only supports a typed version of Exp (no typed version for Dec, Pat, etc).
    • Our previous TH tutorial could not have been written purely with Typed TH, as it heavily uses Dec, for example.
  • Requires that the type being used is known in advance, which may limit the kinds of TH programs you can make.

Before we begin, make sure you have the template-haskell package installed, as well as the TemplateHaskell language extension enabled.

>>> :set -XTemplateHaskell
>>> import Language.Haskell.TH

Typed expressions

In our previous tutorial, we learned that we could use the [e|...|] quoter (which is the same as [|...|]) to create expressions of type Q Exp. With typed TH, we will use [e||...||] (which is the same as [||...||]) to create expressions of type Q (TExp a).

What is TExp a, you might wonder? It’s simply a newtype wrapper around our familiar Exp:

type role TExp nominal
newtype TExp (a :: TYPE (r :: RuntimeRep)) = TExp
  { unType :: Exp
  }

The meaning of the TYPE (r :: RuntimeRep) part is not important to us, but simply put, it allows GHC to describe how to represent some types (boxed, unboxed, etc) during runtime. For more information, see levity polymorphism.

This allows us to use our familiar constructions for Exp, in addition to a type for a which represents the type of the expression. This gives us stronger type-safety mechanisms for our TH application, which will cause the compiler to reject invalid TH programs during their construction.

In the example below, template-haskell gladly accepts 42 :: String using an untyped expression, while the typed counterpart refuses it with a type error.

>>> runQ [|42 :: String|]
SigE (LitE (IntegerL 42)) (ConT GHC.Base.String)

>>> runQ [||42 :: String||]
<interactive>:358:9: error:
    • Could not deduce (Num String) arising from the literal ‘42’
      from the context: Language.Haskell.TH.Syntax.Quasi m
        bound by the inferred type of
                   it :: Language.Haskell.TH.Syntax.Quasi m => m (TExp String)
        at <interactive>:358:1-23In the Template Haskell quotation [|| 42 :: String ||]
      In the first argument of ‘runQ’, namely ‘[|| 42 :: String ||]’
      In the expression: runQ [|| 42 :: String ||]

Typed splices

Just like we had untyped splices such as $foo, now we also have typed splices, written as $$foo. In this section, we will see how to define and splice typed Template Haskell code.

We reiterate one point mentioned in our previous article: before GHC 9, the usage of splices may require parentheses or not. For instance, if foo came from a qualified import, then you’d need to write $$(Foo.foo) instead. In GHC 9, the parser is more relaxed and will not require parentheses in such situations.

Example: calculating prime numbers

As an example, let’s consider the following functions that implement prime number evaluation up to some number. We will make create two versions, one with ordinary Haskell, and another with Template Haskell, so we can see the differences between them. The implementation may be somewhat more verbose than it needs to be to demonstrate the techniques in typed TH and contrast them with an ordinary function.

First, create a file Primes.hs containing two functions: one that checks whether a given a number is prime, and another that generates primes numbers up until some given limit.

module Primes where

isPrime :: Integer -> Bool
isPrime n
  | n <= 1    = False
  | n == 2    = True
  | even n    = False  -- No even number except for 2 is prime
  | otherwise = go 3
  where
    go i
      | i >= n         = True  -- We saw all smaller numbers and no divisors, so it's prime
      | n `mod` i == 0 = False
      | otherwise      = go (i + 2)  -- Iterate through the odd numbers

primesUpTo :: Integer -> [Integer]
primesUpTo n = go 2
  where
    go i
      | i > n     = []
      | isPrime i = i : go (i + 1)
      | otherwise = go (i + 1)

The first function checks whether a number has any divisors. If it has any divisor (apart from 1 and itself), then the number is composite and the function returns False, otherwise it keeps testing for more divisors. If we reach a number that is greater or equal to the input, it means that we have checked all smaller numbers and found no divisors, and so the number is prime, and the function returns True.

The second function simply iterates through the numbers, collecting all primes. We start with 2 since it’s the first prime number.

Keep in mind that these functions are very inefficient, so make sure to use a more optimized version for anything serious!

Now for our Template Haskell version. As usual, let’s create two files, TH.hs and Main.hs, to work with through this example.

Please note that for GHC 9, you will need to write Code Q a in places where we write Q (TExp a). For more details, see the section about Compatibility with GHC 9.

This is what should be in TH.hs:

{-# LANGUAGE TemplateHaskell #-}

module TH where

import Language.Haskell.TH

import Primes (isPrime)

primesUpTo' :: Integer -> Q (TExp [Integer])
--primesUpTo' :: Integer -> Code Q [Integer]
primesUpTo' n = go 2
  where
    go i
      | i > n     = [||[]||]
      | isPrime i = [||i : $$(go (i + 1))||]
      | otherwise = [||$$(go (i + 1))||]

In general, it’s the same thing as the ordinary version. The only difference now being that we return a Q (TExp [Integer]) and generate our list inside the typed expression quoter.

We wrap our recursive calls to go inside splices. Since go has a type of Q (TExp [Integer]), if we didn’t splice it, we’d try to use the cons operator (:) on an Integer and a Q (TExp [Integer]) which would not type-check. An error message might describe the problem quite well:

>>> :l TH
[2 of 2] Compiling TH               ( TH.hs, interpreted )
Failed, no modules loaded.
TH.hs:15:21: error:
    • Couldn't match typeQ (TExp [Integer])’ with ‘[Integer]’
      Expected type: Q (TExp [Integer])
        Actual type: Q (TExp (Q (TExp [Integer])))In the Template Haskell quotation [|| (go (i + 1)) ||]
      In the expression: [|| (go (i + 1)) ||]
      In an equation for ‘go’:
          go i
            | i > n = [|| [] ||]
            | isPrime i = [|| i : $$(go (i + 1)) ||]
            | otherwise = [|| (go (i + 1)) ||]
   |
15 |       | otherwise = [||(go (i + 1))||]
   |                     ^^^^^^^^^^^^^^^^^^

As a matter of fact, we could have written that branch above simply as go (i + 1), without the quoter. Try it!

And now we can use our new function in GHCi like so:

>>> $$(primesUpTo' 100)
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]

We can also inspect it as an untyped Template Haskell definition if we want, using the unType function:

>>> runQ (unType <$> primesUpTo' 10)
InfixE (Just (LitE (IntegerL 2))) (ConE GHC.Types.:) (Just (InfixE (Just (LitE (IntegerL 3))) (ConE GHC.Types.:) (Just (InfixE (Just (LitE (IntegerL 5))) (ConE GHC.Types.:) (Just (InfixE (Just (LitE (IntegerL 7))) (ConE GHC.Types.:) (Just (ConE GHC.Types.[]))))))))

Or, more simply put:

2 : 3 : 5 : 7 : []

Had we made any mistakes in the definition, for example, by using the following definition where we forget a recursive call:

primesUpTo' :: Integer -> Q (TExp [Integer])
--primesUpTo' :: Integer -> Code Q [Integer]
primesUpTo' n = go 2
  where
    go i
      | i > n     = [||[]||]
      | isPrime i = [||i||]  -- We forgot to build a list here
      | otherwise = go (i + 1))

Then we’d be immediately greeted with a type-error:

>>> :r
[2 of 2] Compiling TH               ( TH.hs, interpreted )
Failed, no modules loaded.
TH.hs:14:21: error:
    • Couldn't match typeInteger’ with ‘[a]’
      Expected type: Q (TExp [a])
        Actual type: Q (TExp Integer)In the Template Haskell quotation [|| i ||]
      In the expression: [|| i ||]
      In an equation for ‘go’:
          go i
            | i > n = [|| [] ||]
            | isPrime i = [|| i ||]
            | otherwise = [|| $$(go (i + 1)) ||]
    • Relevant bindings include
        go :: Integer -> Q (TExp [a]) (bound at TH.hs:43:5)
   |
14 |       | isPrime i = [||i||]
   |                     ^^^^^^^

Our primesUpTo' will generate the list of primes at compile-time, and now we can use this list to check the values at runtime.

With this, we can create our Main.hs, where we can try our code:

{-# LANGUAGE TemplateHaskell #-}

import TH

main :: IO ()
main = do
  let numbers = $$(primesUpTo' 10000)
  putStrLn "Which prime number do you want to know?"
  input <- readLn  -- n.b.: partial function
  if input < length numbers
    then print (numbers !! (input - 1))
    else putStrLn "Number too big!"

And that’s it! A very simple program using typed TH. Load Main.hs in GHCi, and after a few seconds when it’s loaded, run our main function. Once asked for an input, type a number such as 200, asking for the 200th prime. The function should output the correct result of 1223.

>>> main
Which prime number do you want to know?
200
1223

Again, our algorithm is quite inefficient and this may take some seconds to compile (as it’s generating numbers as it compiles), and for further improvements, it may be a good idea to have a less naïve algorithm for generating primes, but for educational purposes, it will do for now.

The code used in this post can also be found in this GitHub gist.

A shorter implementation

As mentioned before, we could implement the functions above in a more simple manner, such as:

primesUpTo :: Integer -> [Integer]
primesUpTo n = filter isPrime [2 .. n]

And the corresponding TH function as:

primesUpTo' :: Integer -> Q (TExp [Integer])
--primesUpTo' :: Integer -> Code Q [Integer]
primesUpTo' n = [|| primesUpTo n ||]

And with this, you should be ready to use typed Template Haskell in the wild.

Caveat

Typed Template Haskell may have some difficulties resolving overloads in versions older than GHC 9. Surprisingly, the following does not type-check:

>>> mempty' :: Monoid a => Q (TExp a)
... mempty' = [|| mempty ||]

>>> x :: String
... x = id $$mempty'
<interactive>:549:11: error:
    • Ambiguous type variable ‘a0’ arising from a use of ‘mempty'’
      prevents the constraint ‘(Monoid a0)’ from being solved.
      Probable fix: use a type annotation to specify what ‘a0’ should be.
      These potential instances exist:
        instance Monoid a => Monoid (IO a) -- Defined in ‘GHC.Base’
        instance Monoid Ordering -- Defined in ‘GHC.Base’
        instance Semigroup a => Monoid (Maybe a) -- Defined in ‘GHC.Base’
        ...plus 7 others
        (use -fprint-potential-instances to see them all)
    • In the expression: mempty'
      In the Template Haskell splice $$mempty'
      In the first argument of ‘id’, namely ‘$$mempty'’

Annotating mempty' may resolve it in this case:

>>> x :: String
... x = id $$(mempty' :: Q (TExp String))

>>> x
""

An open ticket exists describing the issue, but if you run into some strange errors, it’s a good idea to keep it in mind.

Compatibility with GHC 9

In GHC 9, the types used by typed Template Haskell were changed according to the Make Q (TExp a) into a newtype proposal.

This proposal changed the typed expression quasi-quoter so it now returns Quote m => Code m a instead of the old Q (TExp a) which was used prior to GHC 9. Besides returing Code m a instead of Q a, the introduction of the Quote type class allows using an arbitrary type instead of the concrete Q from previous versions. Most of the times, Code Q a should suffice for your applications.

If you’re curious, please take a moment to read our Typed Template Haskell in GHC 9 article detailing a migration guide of typed TH code to GHC 9, where we discuss a backward-compatible way of integrating typed Template Haskell code in both GHC 8 and GHC 9.

Thankfully, changing the examples to work with GHC 9 is not difficult. In short, you may replace the parts like Q (TExp a) with Quote m => Code m a (or Code Q a) and the code should compile.

primesUpTo' :: Quote m => Integer -> Code m [Integer]
primesUpTo' n = [|| primesUpTo n ||]

Furthermore, as discussed, the caveat in the previous section doesn’t apply anymore to GHC 9, and you will be able to write the example above.

>>> mempty' :: Monoid a => Code Q a
... mempty' = [|| mempty ||]

>>> x :: String
... x = id $$mempty'

>>> x
""

Should you need to explicitly convert between the old and new types, you may pattern match directly on Code or use the following functions from Language.Haskell.TH:

liftCode :: m (TExp a) -> Code m a
examineCode :: Code m a -> m (TExp a)

Further reading

In this post, we extended our Template Haskell knowledge with TExp. We created a short example where we generated some values during compile time that can be later looked up at runtime. For more resources on typed Template Haskell, check out the following links:

For more Haskell tutorials, you can check out our Haskell articles or follow us on Twitter or Medium.

Work with Serokell if you need Haskell programming services. We offer custom solutions for advanced industries, helping our clients get a competitive edge.

Haskell courses by Serokell
More from Serokell
template haskelltemplate haskell
Lorentz: Implementing Smart Contract eDSL in HaskellLorentz: Implementing Smart Contract eDSL in Haskell
Haskell: A Functional Love StoryHaskell: A Functional Love Story