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

Автор оригинала: Булат Зиганшин
  • Перевод

Данный текст является переводом документации 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
  • +19
  • 5,8k
  • 7
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 7

    0
    Большое спасибо, Template Haskell нужен.

    В примере с заменой printf на конкатенацию имеется в виду, что конкатенация лучше (быстрее, понятнее), чем printf? Сам модуль Text.Printf, кстати, очень любопытен, на первый взгляд там используется какая-то магия.
      +2
      Рад, что Вам интересно.
      Насчёт printf, если уметь пользоваться TH, то написать такой шаблон, преобразующийся в конкатенацию довольно просто. А в Text.Printf используется особая магия с классами типов, чтобы обеспечить переменное число аргументов — сам я не спец в этом, но разберусь на досуге, тоже интересная тема.
      0
      Эээ? А зачем это нужно, когда есть механизм функций высшего порядка? Чего я не понимаю?
        +2
        Лично я TH не пользовался, хватает остальных мощностей.
        Придерживаюсь мнения, «в языке приходится использовать макросы, если остальное слабовато».
        А так например deriveSomeClass может быть полезен. Сгенерировать instance класса бывает достаточно тривиально, но нереально обычными функциями, но ручками это делать каждый раз тоже не комильфо.
          +1
          Извиняйте, случайно ответил не туда (см. ниже)
          +1
          В www.yesodweb.com/ широко используется Template Haskell в купе с квази-цитированием — для шаблонов html/js/css, для роутинга запросов, для описания сущностей в БД.

          Там много холивара по этому поводу идет (использовать ли TH или лучше обходиться чистым хаскелем). И автор написал развернутый пост по этому поводу: www.yesodweb.com/blog/2011/10/code-generation-conversation
          +3
          Собственно, пример с deriveShow вначале довольно нагляден в этом плане. Имеется только определение алгебраического типа и по нему автоматически генерируется instance. Функциями высшего порядка это не сделать — они могут оперировать другими функциями, поскольку функции — это значения (объекты первого класса, если угодно), а код не является объектом первого класса, поэтому для его представления значением и дальнейшего преобразования (генерации) нужны специальные структуры данных, описывающие AST. Простите, если запутанно написал…
          В общем, к аргументу (см. последнее предложение) VoidEx +1 ")

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое