I've know about
Template Haskell (TH) for ages but never used it. As a diversion from another project, I thought I'd try to implement roman numeral literals for Haskell, using TH -- this seems an easy way to get started without having to dig in too far.
I already wrote some
Haskell roman numeral code which gives a function
numeralsToInteger :: String -> Integer
. I tweaked its source code a bit (it wasn't a module previously, for example) and can write this:
import Roman
main = do
putStrLn "roman test 1 (this should print 1998 below)"
print $ numeralsToInteger "MCMXCVIII"
What I want to do with TH is shorten the syntax and make the roman->literal conversion happen at compile time instead of at runtime.
I'll try to use TH's
quasi-quoter syntax, which should allow me to write:
print [ro|MCMXCVIII|]
ro
is the name I've chosen for my quasiquoter, and the
[...|...|]
bracket syntax comes from template haskell quasiquoting.
What will happen is that everything inside those
[|
brackets
|]
will be passed to my code, which can return an arbitrary Haskell expression to be substituted at compile time. Think C-style
#define
on steroids.
In my case, I'll always be returning an integer literal expression. The value of that literal will be determined by parsing the roman numeral.
I need to define
ro :: Language.Haskell.TH.Quote.QuasiQuoter
, and it needs to live in a different module (so that it can be compiled before attempting to parse anything that uses that quasiquoter)
I want to use my quasiquoter to return an expression. They can be used in more contexts than that (for example, when a type is needed). I'll make the other contexts into an
error
, which leaves only the hard one that parses and returns my expression. (after doing a bunch of fiddling Agda, it feels like a relief to be able to say "oh, I'll just make that an error).
It turns out that the code is mostly plumbing.
RomanTH.hs
looks like this:
module RomanTH where
import Roman
import Language.Haskell.TH
import Language.Haskell.TH.Quote
ro :: Language.Haskell.TH.Quote.QuasiQuoter
ro = QuasiQuoter parser err err err
err = error "Roman numeral quasiquoter cannot be used in this context"
parser :: String -> Q Exp
parser s = litE (IntegerL (numeralsToInteger s))
which is all types and wiring except the function
parser s = litE (IntegerL (numeralsToInteger s))
which means (right-to-left) convert string to an Integer, embed it in a literal type, and embed that inside an expression.
The complete test program looks like this:
import RomanTH
main = do
putStrLn "roman test 2 (this should print 1998 below)"
print [ro|MCMXCVIII|]
and runs like this:
$ ./test2
roman test 2 (this should print 1998 below)
1998
Well, that took all of 36 minutes.