Данный текст является переводом документации Template Haskell, написанной Булатом Зиганшиным. Перевод всего текста разбит на несколько логических частей для облегчения восприятия. Далее курсив в тексте — примечания переводчика.
Template Haskell (далее TH) — это расширение языка Haskell предназначенное для мета-программирования. Оно даёт возможность алгоритмического построения программы на стадии компиляции. Это позволяет разработчику использовать различные техники программирования, не доступные в самом Haskell’е, такие как, макро-подобные расширения, направляемые пользователем оптимизации (например inlining), обобщённое программирование (polytypic programming), генерация вспомогательных структур данных и функций из имеющихся. К примеру, код
yell file line = fail ($(printf "Error in file %s line %d") file line)
может быть преобразован с помощью TH в
yell file line = fail ((\x1 x2 -> "Error in file "++x1++" line "++show x2) file line)
Другой пример, код
data T = A Int String | B Integer | C
$(deriveShow ''T)
может быть преобразован в
data T = A Int String | B Integer | C
instance Show T
show (A x1 x2) = "A "++show x1++" "++show x2
show (B x1) = "B "++show x1
show C = "C"
В TH код на Haskell’е генерируется обычными Haskell’евскими функциями (которые я буду для ясности называть шаблонами). Минимум того, что вам необходимо знать, чтобы использовать TH — это следующие темы:
- Как Haskell-код представляется в шаблонах (TH-функциях)
- Как монада цитирования используется для унификации имён
- Как сгенерированный TH-код вставляется в программу
Как Haskell-код представляется в шаблонах
В Template Haskell фрагменты Haskell-кода представляются с помощью обычных алгебраических типов данных. Эти типы построены в соответствии с синтаксисом Haskell и представляют абстрактное синтаксическое дерево (AST — abstract syntax tree) конкретного кода. Есть тип
Exp
для представления выражений, Pat
— для образцов, Lit
— для литералов, Dec
— для объявлений, Type
— для типов и т.д. Определения всех этих типов можно посмотреть в документации модуля Language.Haskell.TH.Syntax
. Они взаимосвязаны в соответствии с правилами синтаксиса Haskell, так что, используя их, можно сконструировать значения представляющие любые фрагменты Haskell-кода. Вот несколько простых примеров:
представляет выражениеvarx = VarE (mkName "x")
x
, т.е. простую переменную “x
”
представляет образецpatx = VarP (mkName "x")
x
, т.е. ту же переменную “x
”, использованную в образце
представляет выражение-константуstr = LitE (StringL "str")
"str"
представляет выражение-пару (кортеж)tuple = TupE [varx, str]
(x,"str")
представляет лямбда-выражениеLamE [patx] tuple
\x -> (x,"str")
Exp
оканчиваются на E
, имена конструкторов типа Pat
— на P
и т.д. Функция mkName
, использованная выше, создаёт значение типа Name
(представляющего идентификаторы) из обычной строки (String
), с её содержанием в качестве имени.Итак, чтобы создать Haskell-код, TH-функция должна просто сконструировать и вернуть значение типа
Exp
(можно ещё Dec
, Pat
или Type
), которое является представлением для данного фрагмента кода. На самом деле, вам не нужно досконально изучать устройство этих типов, чтобы знать, как представить в них нужный Haskell-код, — в разделе об отладке я расскажу, как можно получить TH-представление конкретного фрагмента Haskell-кода.Как монада цитирования используется для унификации имён
Тем не менее шаблоны не являются чистыми функциями, возвращающими простое значение типа
Exp
. Вместо этого они являются вычислениями в специальной монаде Q
(называемой монадой цитирования — “qoutation monad”), которая позволяет автоматически генерировать уникальные имена для переменных с помощью монадической функции newName
:: String -> Q Name
. При каждом её вызове генерируется новое уникальное имя с данным префиксом. Это имя может быть использовано как часть образца (с помощью конструктора VarP
:: Name -> Pat
) или выражения (VarE
:: Name -> Exp
).Давайте напишем простой пример — шаблон
tupleReplicate
, который, будучи использован следующим образом: “$(tupleReplicate n) x"
, вернёт n-местный кортеж с элементом x
на всех позициях (аналогично функции replicate
для списков). Обратите внимание на то, что n
— аргумент шаблона, а x
— аргумент сгенерированной анонимной функции (лямбда-выражения). Я привожу код модуля, содержащего определение этого шаблона (модуль Language.Haskell.TH
предоставляет весь инструментарий, необходимый для работы с TH):
module TupleReplicate where
import Language.Haskell.TH
tupleReplicate :: Int -> Q Exp
tupleReplicate n =
do id <- newName "x"
return $ LamE [VarP id]
(TupE $ replicate n $ VarE id)
К примеру вызов “
tupleReplicate 3
” вернёт значение Exp
эквивалентное Haskell-выражению “(\x -> (x,x,x))
”.Как сгенерированный TH-код вставляется в программу
Вклейка (splice) записывается в виде “
$x
”, где x
— идентификатор, или в виде “$(...)
”, где троеточие подразумевает соответствующее выражение. Важно, чтобы не было пробела между символом $
и идентификатором или скобками. Такое использование $
переопределяет значение этого символа в качестве инфиксного оператора, так же как квалифицированное имя M.x
переопределяет значение оператора композиции функций “.
”. Если нужен именно оператор, то символ нужно окружить пробелами.Вклейка может появляться в
- выражении; вклеиваемое выражение должно иметь тип
Q Exp
. - объявлениях верхнего уровня; вклеиваемое выражение должно иметь тип
Q [Dec]
. Объявления, сгенерированные вклейкой, имеют доступ только к тем идентификаторам, которые объявлены в коде раньше (что нетипично для обычных программ на Haskell’е, в которых порядок объявлений не играет роли). - типе; вклеиваемое выражение должно иметь тип
Q Type
.
Также вам следует знать, что
- при запуске GHC нужно использовать флаг
-XTemplateHakell
, чтобы разрешить специальный синтаксис TH; или можно включить в исходник директиву{-# LANGUAGE TemplateHaskell #-}
. - вы можете использовать шаблон только извне. То есть нельзя определить в одном модуле шаблон и тут же использовать его (вклеить). (это связанно с тем, что шаблон ещё не скомпилирован к этому моменту)
Пример модуля, который использует наш шаблон
tupleReplicate
:
{-# LANGUAGE TemplateHaskell #-}
module Test where
import TupleReplicate
main = do print ($(tupleReplicate 2) 1) -- напечатает (1,1)
print ($(tupleReplicate 5) "x") -- напечатает ("x","x","x","x","x")
Продолжение следует
В последующих частях будут освещены более интересные и продвинутые темы:
- Монада цитирования
- Цитирующие скобки
- Материализация (reification)
- Сообщения об ошибках и восстановление
- Отладка
printf
и deriveShow
.P.S. Это мой первый перевод, так что я надеюсь на конструктивную критику и содержательную дискуссию по теме статьи.
UPDATE:
Часть 2. Инструменты цитирования кода
Часть 3. Прочие аспекты TH