Данный текст является переводом документации 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
