Pull to refresh

Введение в Template Haskell. Часть 1. Необходимый минимум

Reading time4 min
Views9K
Original author: Булат Зиганшин

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

Articles

Change theme settings