Данный текст является переводом документации Template Haskell, написанной Булатом Зиганшиным. Перевод всего текста разбит на несколько логических частей для облегчения восприятия. Далее курсив в тексте — примечания переводчика. Предыдущие части:
Материализация (reification) — это средство Template Haskell, позволяющее программисту получить информацию из таблицы символов компилятора. Монадическая функция
Материализация может быть использована для того, чтобы получить структуру типа, но таким образом нельзя получить тело функции. Если вам нужно материализовать тело функции, то определение функции нужно процитировать и дальше можно будет работать с этим определением с помощью другого шаблона. Например так:
или так
На самом деле, в оригинальной статье больше ничего не говорится про материализацию. Не знаю, насколько это содержательная тема – необходимый минимум знаний о ней ограничивается функцией
Чтобы получить имя (
Эта новая форма тем не менее является цитированием и подчиняется тем же правилам, что и цитирующие скобки
Haskell’евские пространства имён немного всё усложняют. Цитата
Монада цитирования позволяет передавать сообщения об ошибках и восстанавливаться.
Функция
Вызов
Возвращает координаты места в коде (в виде структуры
Для того, чтобы упростить отладку TH-программ, GHC поддерживает специальный флаг
Кроме этого, в интерпретаторе можно запускать вычисления в монаде
Функция
Теперь его можно потестировать в интерпретаторе:
То же самое можно делать и в модулях, которые импортируют шаблоны:
Это небольшой пример, демонстрирующий, как Template Haskell может использоваться для автоматической генерации воплощений (instances) классов. Он использует конструкторы AST, облегчённое цитирование, цитирующие скобки, материализацию – в общем, практически всё, что обсуждалось в этой статье. Для простоты, шаблон работает только для простых алгебраических типов (без параметризации, без именованных полей и т.п.) Я внёс пару изменений (в целях улучшения читабельности) в оригинальный код, в том числе вынес функцию
На этом статья заканчивается, в ней были освещены основные темы в объёме, достаточном для того, чтобы начать использовать метапрограммирование на Haskell, так что можно считать, что введение в Template Haskell состоялось. Для дальнейшего углубления в тему предлагается заглянуть на официальную страничку TH, на которой можно найти ссылки на другие статьи и множество примеров.
Материализация
Материализация (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, на которой можно найти ссылки на другие статьи и множество примеров.