10 June, 2011

roman numeral literals using Template Haskell

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 functionparser 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)

Well, that took all of 36 minutes.

No comments:

Post a Comment