Introduction to Template Haskell. Part 3. Other aspects of TH

Original author: Bulat Ziganshin
  • Transfer
This text is a translation of the Template Haskell documentation written by Bulat Ziganshin. The translation of the entire text is divided into several logical parts to facilitate perception. Further italics in the text are translator's notes. Previous Parts:


Materialization


Materialization (reification) is a Template Haskell tool that allows the programmer to retrieve information from the compiler symbol table. A monadic function returns information about a given name: if it is a global identifier (function, constant, constructor) - you will get its type, if it is a type or class - you will get its structure. Type definition can be found in the module . Materialization can be used to obtain a type structure, but in this way it is impossible to obtain the body of a function. If you need to materialize the body of a function, then you need to quote the definition of the function and you can continue to work with this definition using another template. For example, like this:reify∷ Name → Q InfoInfoLanguage.Haskell.TH.Syntax

$(optimize [d| fib = … |])

or so
fib = $(optimize [| … |])

In fact, the original article says nothing more about materialization. I don’t know how substantial this topic is - the necessary minimum of knowledge about it is limited by function reifyand type Info, but there are some subtleties associated, for example, with the fact that you can not get information about any name. If this topic is interesting, I can collect some information and write a separate note about it (or paste it here).

Lite quoting names


To get the name ( ∷ Name) corresponding to the identifier of interest, you can use the function mkName, but this is not a safe solution, because it mkNamereturns an unqualified name, which can be interpreted differently depending on the context. But the code VarE id ← [| foo |]is safe in this sense, since quoting qualifies names (it turns out something like My.Own.Module.foo), but this code is too verbose and requires a monadic context to use. Fortunately, Template Haskell has another simple form of quoting names: 'foo(a single quote before foo) has a type Nameand contains a qualified name that matches the identifier foo, so the code is let id = 'fooequivalent in meaning to the codeVarE id ← [| foo |]. Note that this construction is of a simple type Name(and not Q Expor Q Name), so that it can be used where monads are not possible, for example:
f ∷ Exp → Exp
f (App (Var m) e) |  m == 'map  =  …

This new form is nonetheless a quotation and follows the same rules as quoting brackets [| … |]. For example, it cannot be used inside these brackets (this is impossible:) [| 'foo |], but gluing to it cannot be applied (this is also impossible:) $( 'foo ), because a type is needed for gluingQ … . More importantly, this form is defined statically, returning a fully qualified name, with an unambiguous interpretation.
Haskell's namespaces complicate things a bit. A quote [| P |]means a data constructor P, while a [t| P |]type constructor means P. Therefore, for “facilitated citation”, the same way of separating these entities is necessary. For type context, just two single quotes are used:
  • 'Foomeans “data constructor Fooin the context of the expression”
  • 'foomeans “name fooin the context of expression”
  • ''Foomeans “type constructor Fooin the context of types”
  • ''foomeans “type variable fooin the context of types”
A lightweight citation form is used in the example of generating incarnations of a class Showthat is parsed at the end.


Error Messages and Recovery


Citation monad allows you to send error messages and recover.
report ∷ Bool → String → Q ()

The function reportdisplays the message given in the second argument. If the first argument True, then the result is perceived as an error, otherwise, the message is simply shown ( as a warning ). In any case, the calculations continue, if they need to be stopped, use the monad fail. If recoverthere is no “close” , compilation fails.
recover ∷ Q a → Q a → Q a

The call recover a bstarts b. If there bwere calls report True "…", then it starts a. If bterminated without such errors, then its result is returned, but aignored.
location ∷ Q Loc

Returns the coordinates of the place in the code (in the form of a structure Loc) where the current gluing occurs - it can be convenient for error messages.

Debugging


In order to simplify the debugging of TH programs, the GHC supports a special flag -ddump-spliceswith which it shows the results of pasting all the top-level templates during module loading.
In addition, in the interpreter, you can run calculations in the monad Qusing the function and display the result either in the form of abstract syntax (AST) or in the form of the corresponding Haskell code:runQ∷ Q a → IO a
$ ghci –XTemplateHaskell
…
Prelude> :m + Language.Haskell.TH 
Prelude Language.Haskell.TH> runQ [| \x _ -> x |] >>= print
LamE [VarP x_1,WildP] (VarE x_1)
Prelude Language.Haskell.TH> runQ [| \x _ -> x |] >>= putStrLn . pprint 
\x_0 _ -> x_0

The function pprintdisplays the Haskell code as it will be pasted into the program during compilation. For further examples, we write a simple template that generates a lambda expression that ignores its arguments and returns the given string:
module Cnst where
import Language.Haskell.TH
cnst :: Int -> String -> Q Exp
cnst n s = return (LamE (replicate n WildP) (LitE (StringL s)))

Now it can be tested in the interpreter:
$ ghci -XTemplateHaskell Cnst.hs  
…
*Cnst> runQ (cnst 2 "str") >>= print
LamE [WildP,WildP] (LitE (StringL "str"))
*Cnst> runQ (cnst 2 "str") >>= putStrLn . pprint 
\_ _ -> "str"

The same thing can be done in modules that import templates:
{-# LANGUAGE TemplateHaskell #-}
module Main where
import Language.Haskell.TH
import Cnst
-- шаблон cnst можно вклеивать как выражение:
cnst1 ::  t -> [Char]
cnst1 = $(cnst 1 "x")
cnst2 ::  t1 -> t2 -> [Char]
cnst2 = $(cnst 2 "str")
-- аналогично, будет тип с 20ю аргументами
cnst20 = $(cnst 20 "foo")
-- так же как в интерпретаторе, можно использовать runQ:
main = do runQ(cnst 1 "x")    >>= print
          runQ(cnst 2 "str")  >>= print
          runQ(cnst 20 "foo") >>= print
          runQ(cnst 1 "x")    >>= putStrLn.pprint
          runQ(cnst 2 "str")  >>= putStrLn.pprint
          runQ(cnst 20 "foo") >>= putStrLn.pprint


Example: deriveShow



This is a small example demonstrating how Template Haskell can be used to automatically generate instances of classes. It uses AST constructors, lightweight quoting, quoting brackets, materialization - in general, almost everything that was discussed in this article. For simplicity, the template only works for simple algebraic types (without parameterization, without named fields, etc.). I made a couple of changes (in order to improve readability) in the original code, including a function showClauseseparately.
  • Main.hs

    {-# LANGUAGE TemplateHaskell #-}
    module Main where
    import Derive
    data T = A Int String | B Integer | C
    $(deriveShow ''T)
    main = print [A 1 "s", B 2, C]  -- печатает в точности [A 1 "s",B 2,C]

  • Derive.hs

    {-# LANGUAGE TemplateHaskell #-}
    module Derive where
    import Language.Haskell.TH
    import Control.Monad
    data T1 = T1
    -- Вспомогательная функция, которая генерирует 
    -- n уникальных имён для образцов и выражений
    genPE :: Int -> Q ([PatQ], [ExpQ])
    genPE n = do
        ids <- replicateM n (newName "x")
        return (map varP ids, map varE ids)
    -- Собираем один клоз функции show для данного конструктора:
    --   show (A x1 x2) = "A "++show x1++" "++show x2
    showClause :: Con -> Q Clause
    showClause (NormalC name fields) = do
        -- Имя конструктора, т.е. "A". nameBase возвращает имя без квалификации
        let constructorName = nameBase name
        -- Генерируем имена переменных для левой и правой части определения 
        (pats,vars) <- genPE (length fields)
        -- Рекурсивно строим выражение (" "++show x1++...++"") из списка переменных [x1, ...]
        let f []       = [| constructorName |]
            f (v:vars) = [| " " ++ show $v ++ $(f vars) |]
        -- Собираем один клоз функции
        clause [conP name pats]       -- (A x1 x2)
               (normalB (f vars)) []  -- "A"++" "++show x1++" "++show x2
    -- Основной шаблон, который генерирует объявление воплощения класса Show
    deriveShow :: Name -> Q [Dec]
    deriveShow t = do
        -- Получаем список конструкторов данных для типа t
        TyConI (DataD _ _ _ constructors _) <- reify t
        -- Теперь собираем все клозы в тело функции show:
        --   show (A x1 x2) = "A "++show x1++" "++show x2
        --   show (B x1)    = "B "++show x1
        --   show C         = "C"
        showbody <- mapM showClause constructors
        -- Генерируем шаблон объявления воплощения и потом заменяем в нём
        -- имя типа (T1) и тело функции (x = "text") нашим сгенерированным showbody
        d <- [d| instance Show T1 where
                    show x = "text"
              |]
        let    [InstanceD [] (AppT showt (ConT _T1)) [FunD showf _text]] = d
        return [InstanceD [] (AppT showt (ConT t  )) [FunD showf showbody]]


Conclusion


This article ends here, it covered the main topics in an amount sufficient to start using metaprogramming in Haskell, so we can assume that the introduction to Template Haskell has taken place. For further insight into the topic, it is proposed to look at the official page of TH , where you can find links to other articles and many examples.

Also popular now: