Template Haskell 101

As of March 2020, School of Haskell has been switched to read-only mode.

Learning Template Haskell has been on my Fun List for quite a while, but for some reason I never got to it. Recently however, while looking for a reasonable way to include multiline strings in Haskell programs, I stumbled upon, at first strange advice: use Template Haskell.

So I put together this simple test program (mostly copying from the wikipage mentioned above):

{-# START_FILE Str.hs #-}
module Str(str) where
 
import Language.Haskell.TH
import Language.Haskell.TH.Quote
 
str = QuasiQuoter { quoteExp = stringE }

{-# START_FILE Main.hs #-}
{-# LANGUAGE QuasiQuotes #-}
module Main where
import Str
 
longString = [str|This is a multiline string.
It's many lines long.
 
 
It contains embedded newlines. And Unicode:
 
Ἐν ἀρχῇ ἦν ὁ Λόγος
 
It ends here: |]
 
main = putStrLn longString
   

This sparked my curiosity (hey, I need to understand this), and finally gave me the excuse to learn TH. The QuasiQuote stuff (which is an extension of TH) I will get to later, first let's start with TH itself.

One tutorial advised exploring in GHCi (whih I love to do) so I started with that.

$ ghci -XTemplateHaskell

> :m +Language.Haskell.TH
> runQ [| \x -> 1 |]

LamE [VarP x_0] (LitE (IntegerL 1))

> :t it
it :: Exp

So we can (kind of) parse Haskell at runtime, which is good and well but what is more interesting is the other direction: we can construct a syntax tree and inject (aka ``splice'') it into our program

> runQ [| succ 1 |]
AppE (VarE GHC.Enum.succ) (LitE (IntegerL 1))
Prelude Language.Haskell.TH> $(return it)
2

> $(return (LitE (IntegerL 42)))
42

names are just a trifle more involved

> $( return (AppE (VarE (mkName "succ")) (LitE (IntegerL 1))))
2

So far we've been only constructing expressions, but other syntax elements, like patterns and declarations can be constructed too:

> runQ [d| p1 (a,b) = a |]
[FunD p1_0 [Clause [TupP [VarP a_1,VarP b_2]] (NormalB (VarE a_1)) []]]

For technical reasons, definitions cannot be used in the same module they are built, so we need at least two modules to demonstrate this in a real program:

{-# START_FILE Build1.hs #-}
{-# LANGUAGE TemplateHaskell #-}
module Build1 where
import Language.Haskell.TH

build_p1 :: Q [Dec]
build_p1 = return
    [ FunD p1 
             [ Clause [TupP [VarP a,VarP b]] (NormalB (VarE a)) []
             ]
    ] where
       p1 = mkName "p1"
       a = mkName "a"
       b = mkName "b"
       
{-# START_FILE Declare1.hs #-}       
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
import Build1

$(build_p1)

main = print $ p1 (1,2)

I will not explain what FunD etc means, and instead refer you to the documentation.

Building and transforming syntax trees for a language with bindings is usually a pain, because you have to keep remembering to avoid capture. Luckily, TH makes it easy with the function newName:

newName :: String -> Q Name

(which, by the way explains one of the reasons why Q needs to be a monad).

So let us make our example capture-proof (even if it seems unnecessary right now). Note that p1 is a global name and still needs to be named just that with mkName, while a and b can be any names, so we generate them with newName

{-# START_FILE Build2.hs #-}
{-# LANGUAGE TemplateHaskell #-}
module Build2 where
import Language.Haskell.TH
--show
build_p1 :: Q [Dec]
build_p1 = do
  let p1 = mkName "p1"  
  a <- newName "a"
  b <- newName "b"
  return
    [ FunD p1 
             [ Clause [TupP [VarP a,VarP b]] (NormalB (VarE a)) []
             ]
    ]
--/show    

{-# START_FILE Declare2.hs #-}  
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
--show
import Build2

$(build_p1)

main = print $ p1 (1,2)

What we have done so far served little purpose (except purely educational), but let us tackle a real problem: define all projections for big (say 16-) tuples. Doing this manually would be obviously a major PITA, but Template Haskell can help.

Actually, let us start smaller and define both projections for pairs first. Extending this to any number is then a simple exercise.

First we might want abstract declaring a simple function, e.g.

simpleFun name pats rhs = FunD name [Clause pats (NormalB rhs) []]

Then, if I have a function such that build_p n builds nth definition, I can build all $n$ using mapM:

build_ps = mapM build_p [1,2]
{-# START_FILE Build3.hs #-}
{-# LANGUAGE TemplateHaskell #-}
module Build3 where
import Language.Haskell.TH

simpleFun :: Name -> [Pat] -> Exp -> Dec
simpleFun name pats rhs = FunD name [Clause pats (NormalB rhs) []]

--show
build_ps = mapM build_p [1,2] where
    fname n = mkName $ "p2_" ++ show n
    argString k = "a" ++ show k
    argStrings = map argString [1,2]
    build_p n = do    
        argNames <- mapM newName argStrings
        let args = map VarP argNames
        return $ simpleFun (fname n) [TupP args] (VarE (argNames !! (n-1)))
--/show
{-# START_FILE Declare3.hs #-}  
{-# LANGUAGE TemplateHaskell #-}

import Language.Haskell.TH

import Build3
--show
$(build_ps)

main = mapM_ print 
  [ p2_1 (1,2)
  , p2_2 (1,2)
  ]
--/show

In the next episode we will look at quasiquotation.