A Short Overview of Typed Template Haskell

serokell

Serokell

Posted on September 29, 2021

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.

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

Enter fullscreen mode Exit fullscreen mode

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
  }

Enter fullscreen mode Exit fullscreen mode

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-23
    • In the Template Haskell quotation [|| 42 :: String ||]
      In the first argument of ‘runQ’, namely ‘[|| 42 :: String ||]’
      In the expression: runQ [|| 42 :: String ||]

Enter fullscreen mode Exit fullscreen mode

Typed splices

Just like we had untyped splices such as $foo, now we also have typed splices, written as $$foo. Note, however, that if your GHC version is below 9.0, you may need to write $$(foo) instead.

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)

Enter fullscreen mode Exit fullscreen mode

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.

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' n = go 2
  where
    go i
      | i > n = [||[]||]
      | isPrime i = [||i : $$(go (i + 1))||]
      | otherwise = [||$$(go (i + 1))||]

Enter fullscreen mode Exit fullscreen mode

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 type ‘Q (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))||]
   | ^^^^^^^^^^^^^^^^^^

Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

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.[]))))))))

Enter fullscreen mode Exit fullscreen mode

Or, more simply put:

2 : 3 : 5 : 7 : []

Enter fullscreen mode Exit fullscreen mode

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' n = go 2
  where
    go i
      | i > n = [||[]||]
      | isPrime i = [||i||] -- We forgot to build a list here
      | otherwise = go (i + 1))

Enter fullscreen mode Exit fullscreen mode

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 type ‘Integer’ 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||]
   | ^^^^^^^

Enter fullscreen mode Exit fullscreen mode

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!"

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

And the corresponding TH function as:

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

Enter fullscreen mode Exit fullscreen mode

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. 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')’

Enter fullscreen mode Exit fullscreen mode

Annotating mempty' may resolve it in this case:

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

>>> x
""

Enter fullscreen mode Exit fullscreen mode

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.

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.

💖 💪 🙅 🚩
serokell
Serokell

Posted on September 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related