Данный текст является переводом документации 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 — это следующие темы:
  1. Как Haskell-код представляется в шаблонах (TH-функциях)
  2. Как монада цитирования используется для унификации имён
  3. Как сгенерированный 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")

Продолжение следует


В последующих частях будут освещены более интересные и продвинутые темы:
  1. Монада цитирования
  2. Цитирующие скобки
  3. Материализация (reification)
  4. Сообщения об ошибках и восстановление
  5. Отладка
и разобраны упомянутые вначале примеры printf и deriveShow.

P.S. Это мой первый перевод, так что я надеюсь на конструктивную критику и содержательную дискуссию по теме статьи.

UPDATE:
Часть 2. Инструменты цитирования кода
Часть 3. Прочие аспекты TH