TemplateHaskell crimes

24 November 2022

Sometimes when working with TemplateHaskell, you would like to attach some additional meta information to data constructors. For example, I came up with this when working on a compiler, where I had a function to pretty print expressions with a case for every single expression.

Writing out a new case for every single constructor in a different part of the file was getting quite annoying, so I wanted to use TemplateHaskell to make this a bit simpler. If this had been in Rust, I could have written the pretty printing information in doc comments like this:

data Expr 
    -- | pretty: $0
    = Var Name
    -- | pretty: λ$0. $1
    | Lambda Name Expr
    -- | pretty: ($0) ($1)
    | App Expr Expr

Unfortunately, this doesn’t work in Haskell, since TemplateHaskell doesn’t have access to Haddock comments.

There is another way though!

Did you know that GADT constructors don’t actually need to return the type they are defining?

For example: in this code, Florb doesn’t actually need to return something of type Flurb!

data Flurb where
     Florb :: Int -> Flurb

It only needs to return something that unifies with Flurb.

This compiles!

type DefinitelyNotFlurb = Flurb

data Flurb where
     Florb :: Int -> DefinitelyNotFlurb

We don’t need to constrain ourselves to simple type aliases like this. We can even add a type parameter to our synonym, and TemplateHaskell will see the exact type we wrote down, ignored type parameters and everything!

With this, we can finally add prettyprinting annotations to our Expr type.

type PrettyAnn :: Symbol -> Type -> Type
type PrettyAnn s a = a

data Expr where
    Var    :: Name -> PrettyAnn "$0" Expr
    Lambda :: Name -> Expr -> PrettyAnn " λ$0. $1" Expr
    App    :: Expr -> Expr -> PrettyAnn "($0) ($1)" Expr
    deriving (Eq)

Even better: GHC can see that these aren’t real GADTs, so deriving clauses still just work.