Haskell Quasiquotation
For BayesHive, one of the things we need to do a lot of is program transformation. The main reason for this is to generate code to represent probability distribution functions derived from descriptions of probability models–the models are described using code written in the probability monad, and this needs to be manipulated quite extensively to determine the PDF, which is needed for the Markov chain Monte Carlo sampling we use for estimating parameters in probability models.
Program transformation is something that Haskell is really great at, since it basically just involves pattern matching against lots of different cases of syntax trees in the language you’re transforming. Unfortunately, this leads to lots of code that looks like this (picking a simple case at random):
... case last rvs of Observed (EApp (EApp (EVar "map") (EVar f)) (EVar nm)) -> rewriteFst f nm _ -> rvs ...
All that stuff with EApp
and EVar
is the explicit AST representation of the expression map f nm
in the language (Baysig) that we’re manipulating.
At first, doing things like this is pretty unavoidable, but wouldn’t it be nice if you could write something like this instead?
... case last rvs of Observed [baysig|map $f $nm|] -> rewriteFst f nm _ -> rvs ...
Well, with the magic of Haskell quasiquotation, that’s exactly what you can do. I’d been looking at all this AST pattern matching stuff for a while, thinking “Oh, I wish we had a quasiquoter”, but kept shying away from trying to write one. I’d never done it before, and it seemed like it might not be very simple. However, it turned out to be easy as pie (at least to get a first, sort of useful, version going).
Assuming you already have a parser for your language (we do, of course), adapting it for use within a quasiquoter turns out to be pretty simple. You need to add some stuff to your AST data types to represent anti-quoters ($f
and $nm
in the pattern above, used to represent Haskell names for Baysig AST values within a quasiquoted Baysig expression) and rejig your parser to handle them.
Then you set up your quasiquoter. This is just a value that looks like:
baysig :: QuasiQuoter baysig = QuasiQuoter { quoteExp = baysigEToTHExp , quotePat = baysigEToTHPat , quoteType = error "No type quoter" , quoteDec = error "No declaration quoter" }
where the functions baysigEToTHExp
and baysigEToTHPat
have types String -> Q Exp
and String -> Q Pat
respectively, i.e. they take a string (which is everything inside the [baysig|...|]
brackets) and generate a Template Haskell expression (Exp
) or pattern (Pat
) value (the Q
thing is a monad that provides unique name generation if you need it).
Of course, our Baysig parser doesn’t return a Template Haskell Exp
value, but Language.Haskell.TH.Quote
provides a couple of helper functions to turn Haskell values (so long as they’re instances of Data
) into TH expressions or patterns. These functions have rather inscrutable types:
dataToExpQ :: Data a => (forall b. Data b => b -> Maybe (Q Exp)) -> a -> Q Exp dataToPatQ :: Data a => (forall b. Data b => b -> Maybe (Q Pat)) -> a -> Q Pat
The first function argument is for dealing with special cases (the documentation calls them “type-specific cases”) and allows you to intercept the data-to-TH conversion machinery to deal with things like anti-quoters. There’s a neat little package called antiquoter
that makes this really easy in a kind of “scrap your boilerplate” way. You define some functions that take values of your input AST type and either return Nothing
or Just
a TH value. Here, we deal with anti-quoter AST nodes, just turning them into TH name references; everything else we ignore and return Nothing
:
antiExpP :: E -> Maybe (Q Pat) antiExpP (EAntiVar s) = Just . varP $ mkName s antiExpP _ = Nothing
The antiquoter
package then provides some combinators to wrap these functions up into a function suitable for use with the dataToExpQ
and dataToPatQ
functions:
antiP :: AntiQuoter Pat antiP = antiExpP <>> const Nothing
Once you have this, dealing with quasiquotation is just a matter of calling your parser and if the parse is successful, passing the result to dataToExpQ
or, as here, dataToPatQ
:
baysigEToTHPat :: String -> Q Pat baysigEToTHPat s = case parseQuoteEs s of Left err -> error $ "Parse failed in baysig quasiquoter pattern: " ++ err Right exp -> dataToPatQ antiP exp
Easy-peasy. And then you can use these things in pattern matches or to build more complex expression from smaller ones:
let xval = [baysig|y+1|] fval = [baysig|f x|] expr2 = [baysig|let x = $xval in $fval|]
Cute, eh? (And yes, the Baysig parser has a layout rule too, which works perfectly happily when Baysig code is embedded within Haskell like this!)