Here's a practical example of applying Template Haskell to reduce the amount of boilerplate code that is otherwise required. I wrote the below after following this excellent blog post by Matt Parsons. This post will be much higher-level, read Matt's blog for the gorier details. Liquorice Liquorice is a toy project of mine from a few years ago that lets you draw 2D geometric structures similar to LOGO. Liquorice offers two interfaces: pure functions that operate on an explicit
Context(the pen location: existing lines, etc.), and a second "stateful" interface where the input and output are handled in the background. I prefix the pure ones
P.and the stateful ones
S.in this blog post for clarity. The stateful interface can be much nicer to use for larger drawings. Compare example8b.hs, written in terms of the pure functions, and the stateful equivalent example8.hs. The majority of the stateful functions are "wrapped" versions of the pure functions. For example, the pure function
P.steptakes two numbers and moves the pen forward and sideways. Its type signature is
Here's the signature and implementation of the stateful equivalent:
P.step :: Int -> Int -> Context -> Context
Writing these wrapped functions for the 29 pure functions is boilerplate that can be generated automatically with Template Haskell. Generating the wrapper functions Given the
S.step :: Int -> Int -> State Context () S.step x y = modify (P.step x y)
Nameof a function to wrap, we construct an instance of
FunD, the TH data-type representing a function definition. We use the base name of the incoming function as the name for the new one.
To determine how many arguments the wrapper function needs to accept, we need to determine the input function's arity. We use Template Haskell's
mkWrap fn = do let name = mkName (nameBase fn) return $ FunD name [ Clause (map VarP args) (NormalB rhs)  ]
reifyfunction to get type information about the function, and derive the arity from that. Matt Parson's covers this exactly in his blog.
We can use the list "args" directly in the clause part of the function definition, as the data-type expects a list. For the right-hand side, we need to convert from a list of arguments to function application. That's a simple left-fold:
info <- reify fn let ty = (\(VarI _ t _ ) -> t) info let n = arity ty - 1 args <- replicateM n (newName "arg")
We use TH's oxford brackets for the definition of
-- mkFnApp f [a,b,c] => ((f a) b) c => f a b c mkFnApp = foldl (\e -> appE e . varE) rhs <- [ modify $(mkFnApp (varE fn) args) :: State Context () ]
rhs. This permits us to write real Haskell inside the brackets, and get an expression data-type outside them. Within we have a splice (the
$( )), which does the opposite: the code is evaluated at compile time and generates an
Expthat is then converted into the equivalent Haskell code and spliced into place. Finally, we need to apply the above to a list of
Names. Sadly, we can't get at the list of exported names from a Module automatically. There is an open request for a TH extension for this. In the meantime, we export a list of the functions to wrap from the
Puremodule and operate on that
Finally, we 'call'
import Liquorice.Pure wrapPureFunctions = mapM mkWrap pureFns
wrapPureFunctionsat the top level in our state module and Template Haskell splices all the function definitions into place. The final code ended up only around 30 lines of code, and saved about the same number of lines of boilerplate. But in doing this I noticed some missing functions, and it will pay dividends if more pure functions are added. Limitations The current implementation has one significant limitation: it cannot handle higher-order functions. An example of a pure higher-order function is
place, which moves the pen, performs an operation, and then moves it back:
Wrapping this is not sufficient because the higher-order parameter has the pure function signature
P.place :: Int -> Int -> (Context -> Context) -> Context -> Context
Context -> Context. If we wrapped it, the stateful version of the function would accept a pure function as the parameter, but you would expect it to accept another stateful function. To handle these, at a minimum we would need to detect the function arguments that have type
Context -> Contextand replace them with
State Context (). The right-hand side of the wrapped function would also need to do more work to handle wrapping and unwrapping the parameter. I haven't spent much time thinking about it but I'm not sure that a general purpose wrapper would work for all higher-order functions. For the time being I've just re-implemented the half-dozen of them.