TemplateHaskell crimes
24 November 2022Sometimes 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.