Материализация
Материализация (reification) — это средство Template Haskell, позволяющее программисту получить информацию из таблицы символов компилятора. Монадическая функция
reify ∷ Name → Q Info возвращает информацию о данном имени: если это глобальный идентификатор (функция, константа, конструктор) – вы получите его тип, если это тип или класс – вы получите его структуру. Определение типа Info можно найти в модуле Language.Haskell.TH.Syntax.Материализация может быть использована для того, чтобы получить структуру типа, но таким образом нельзя получить тело функции. Если вам нужно материализовать тело функции, то определение функции нужно процитировать и дальше можно будет работать с этим определением с помощью другого шаблона. Например так:
$(optimize [d| fib = … |])
или так
fib = $(optimize [| … |])
На самом деле, в оригинальной статье больше ничего не говорится про материализацию. Не знаю, насколько это содержательная тема – необходимый минимум знаний о ней ограничивается функцией
reify и типом Info, но есть некоторые тонкости, связанные например с тем, что можно получить информацию не о любом имени. Если эта тема интересна, я могу собрать какую-нибудь информацию и написать об этом отдельную заметку (или вклеить сюда).Облегчённое цитирование имён
Чтобы получить имя (
∷ Name), соответствующее интересующему идентификатору, можно использовать функцию mkName, но это не безопасное решение, потому что mkName возвращает не квалифицированное имя, которое может интерпретироваться по-разному в зависимости от контекста. А вот код VarE id ← [| foo |] безопасен в этом смысле, так как цитирование квалифицирует имена (получится что-то типа My.Own.Module.foo), но этот код слишком многословный и требует монадический контекст для использования. К счастью, Template Haskell, имеет другую простую форму цитирования имён: 'foo (одинарная кавычка перед foo) имеет тип Name и содержит квалифицированное имя, соответствующее идентификатору foo, так что код let id = 'foo эквивалентен по смыслу коду VarE id ← [| foo |]. Обратите внимание, что эта конструкция имеет простой тип Name (а не Q Exp или Q Name), так что она может быть использована там, где не возможно использование монад, например:f ∷ Exp → Exp f (App (Var m) e) | m == 'map = …
Эта новая форма тем не менее является цитированием и подчиняется тем же правилам, что и цитирующие скобки
[| … |]. Например, она не может быть использована внутри этих скобок (так нельзя: [| 'foo |]), но и вклеивание к ней не может быть применено (так тоже нельзя: $( 'foo )), потому что для вклейки нужен тип Q …. Более важно то, что эта форма определяется статически, возвращая полностью квалифицированное имя, с однозначной интерпретацией.Haskell’евские пространства имён немного всё усложняют. Цитата
[| P |] означает конструктор данных P, в то время как [t| P |] означает конструктор типа P. Поэтому для “облегчённого цитирования” необходим такой же способ разделения этих сущностей. Для контекста типов используется просто две одинарные кавычки:'Fooозначает “конструктор данныхFooв контексте выражения”'fooозначает “имяfooв контексте выражения”''Fooозначает “конструктор типаFooв контексте типов”''fooозначает “переменная типаfooв контексте типов”
Show, который разбирается в конце.Сообщения об ошибках и восстановление
Монада цитирования позволяет передавать сообщения об ошибках и восстанавливаться.
report ∷ Bool → String → Q ()
Функция
report выводит данное во втором аргументе сообщение. Если первый аргумент True, то результат воспринимается как ошибка, иначе, сообщение просто показывается (как warning). В любом случае, вычисления продолжаются, если их нужно остановить, используйте монадный fail. Если “закрывающего” recover нет, компиляция завершается неудачно.recover ∷ Q a → Q a → Q a
Вызов
recover a b запускает b. Если в b были вызовы report True "…", то запускается a. Если b завершается без таких ошибок, то возвращается его результат, а a игнорируется.location ∷ Q Loc
Возвращает координаты места в коде (в виде структуры
Loc), где происходит текущее вклеивание – может быть удобно для сообщений об ошибках.Отладка
Для того, чтобы упростить отладку TH-программ, GHC поддерживает специальный флаг
-ddump-splices, с которым он показывает результаты вклейки всех шаблонов верхнего уровня во время загрузки модуля.Кроме этого, в интерпретаторе можно запускать вычисления в монаде
Q с помощью функции runQ ∷ Q a → IO a и выводить результат либо в виде абстрактного синтаксиса (AST), либо в виде соответствующего Haskell-кода:$ 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
Функция
pprint выводит Haskell-код в том виде, как он будет вклеен в программу при компиляции. Для дальнейших примеров напишем простой шаблон, который генерирует лямбда-выражение, игнорируещее свои аргументы и возвращающее данную строку:module Cnst where import Language.Haskell.TH cnst :: Int -> String -> Q Exp cnst n s = return (LamE (replicate n WildP) (LitE (StringL s)))
Теперь его можно потестировать в интерпретаторе:
$ ghci -XTemplateHaskell Cnst.hs … *Cnst> runQ (cnst 2 "str") >>= print LamE [WildP,WildP] (LitE (StringL "str")) *Cnst> runQ (cnst 2 "str") >>= putStrLn . pprint \_ _ -> "str"
То же самое можно делать и в модулях, которые импортируют шаблоны:
{-# 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
Пример: deriveShow
Это небольшой пример, демонстрирующий, как Template Haskell может использоваться для автоматической генерации воплощений (instances) классов. Он использует конструкторы AST, облегчённое цитирование, цитирующие скобки, материализацию – в общем, практически всё, что обсуждалось в этой статье. Для простоты, шаблон работает только для простых алгебраических типов (без параметризации, без именованных полей и т.п.) Я внёс пару изменений (в целях улучшения читабельности) в оригинальный код, в том числе вынес функцию
showClause отдельно.- 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]]
Заключение
На этом статья заканчивается, в ней были освещены основные темы в объёме, достаточном для того, чтобы начать использовать метапрограммирование на Haskell, так что можно считать, что введение в Template Haskell состоялось. Для дальнейшего углубления в тему предлагается заглянуть на официальную страничку TH, на которой можно найти ссылки на другие статьи и множество примеров.

