
Introduction to Template Haskell. Part 3. Other aspects of TH
- 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 (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:
or so
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
To get the name (
This new form is nonetheless a quotation and follows the same rules as quoting brackets
Haskell's namespaces complicate things a bit. A quote
Citation monad allows you to send error messages and recover.
The function
The call
Returns the coordinates of the place in the code (in the form of a structure
In order to simplify the debugging of TH programs, the GHC supports a special flag
In addition, in the interpreter, you can run calculations in the monad
The function
Now it can be tested in the interpreter:
The same thing can be done in modules that import templates:
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
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.
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 Info
Info
Language.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
reify
and 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 mkName
returns 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 Name
and contains a qualified name that matches the identifier foo
, so the code is let id = 'foo
equivalent in meaning to the codeVarE id ← [| foo |]
. Note that this construction is of a simple type Name
(and not Q Exp
or 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:'Foo
means “data constructorFoo
in the context of the expression”'foo
means “namefoo
in the context of expression”''Foo
means “type constructorFoo
in the context of types”''foo
means “type variablefoo
in the context of types”
Show
that 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
report
displays 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 recover
there is no “close” , compilation fails.recover ∷ Q a → Q a → Q a
The call
recover a b
starts b
. If there b
were calls report True "…"
, then it starts a
. If b
terminated without such errors, then its result is returned, but a
ignored.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-splices
with 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
Q
using 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
pprint
displays 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
showClause
separately.- 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.