Как стать автором
Обновить

Введение в Template Haskell. Часть 2. Инструменты цитирования кода

Время на прочтение6 мин
Количество просмотров3K
Автор оригинала: Булат Зиганшин
Данный текст является переводом документации Template Haskell, написанной Булатом Зиганшиным. Перевод всего текста разбит на несколько логических частей для облегчения восприятия. Далее курсив в тексте — примечания переводчика. Другие части:


Монада цитирования


Поскольку шаблоны должны возвращать свои значения обёрнутыми в монаду Q, для этого имеется набор вспомогательных функций, которые “поднимают” (оборачивают в Q) конструкторы типов Exp, Lit, Pat: lamE (соотв. LamE), varE, appE, varP и т.д. В их сигнатурах так же используются переобозначенные поднятые типы: ExpQ = Q Exp, LitQ = Q Lit, PatQ = Q Pat… (все их можно найти в модуле Language.Haskell.TH.Lib). Используя эти функции, можно значительно сократить код, реже используя do-синтаксис.
В TH также есть функция lift, которая поднимает до Q Exp значение любого типа из класса Lift.
В некоторых редких случаях, вам может понадобиться не генерация уникального имени, а использование точного имени идентификатора из внешнего (по отношению к шаблону) кода. Для этих целей есть (чистая) функция mkName ∷ String → Name. Есть также вспомогательная функция dyn s = return (VarE (mkName s)), которая возвращает значение Exp представляющее переменную с данным именем (dyn ∷ String → Q Exp).

Цитирующие скобки


Построение значений Exp, представляющих абстрактное синтаксическое дерево — трудоёмкая и скучная работа. Но к счастью, в Template Haskell есть цитирующие скобки, которые преобразуют конкретный Haskell-код в структуру, представляющую его.
Они бывают четырёх типов:
  • [e| … |] или [| … |] для выражений (∷ Q Exp)
  • [d| … |] для объявлений (∷ Q [Dec])
  • [t| … |] для типов (∷ Q Type)
  • [p| … |] для образцов (паттернов) (∷ Q Pat)

Соответственно внутри скобок должно быть синтаксически корректное выражение/объявление/тип/образец.
Например цитата [| λ _ → 0 |] представляет собой структуру (return $ LamE [WildP] (LitE (IntegerL 0))). Цитата имеет тип Q Exp (а не просто Exp), так что она должна быть вычислена внутри монады цитирования, что позволяет Template Haskell заменить все идентификаторы, появляющиеся внутри цитаты, на уникальные, сгенерированные с помощью newName. Например, цитата [| λx → x |] будет преобразована в такой код:
do id ← newName "x"
   return $ LamE [VarP id] (VarE id)

Далее, внутри цитирующих скобок мы можем использовать вклейку (сплайсинг), так что получается, что TH выступает в роли макро-препроцессора, обрабатывающего часть кода написанного явно и часть сгенерированного. Например, цитата [| 1 + $(f x) |] вычислит (f x), которая должна иметь тип Q Exp, выражение (структуру типа Exp), получившееся в результате представит в виде обычного Haskell-кода и подставит (вклеит) его на место $(f x), а потом продолжит цитирование — преобразование получившегося кода в структуру, представляющую его. Благодаря автоматическому переименовыванию (собственно, для этого всё и делается внутри монады Q), внутри цитаты не будет конфликтов имён локальных переменных между разными вклейками одного и того же кода. Следующее определение хорошо это демонстрирует:
summ ∷ Int → Q Exp
summ n = summ' n [| 0 |]

summ' ∷ Int → Q Exp → Q Exp
summ' 0 code = code
summ' n code = [| λx → $(summ' (n-1) [| $code + x |]) |]

Этот шаблон генерирует лямбда-выражение с n параметрами, которое их суммирует. Например, $(summ 3) преобразуется в (λx1 → λx2 → λx3 → 0 + x1 + x2 + x3). Обратите внимание, на то, что в сгенерированном коде используются разные идентификаторы для аргументов вложенных лямбда-выражений, хотя в шаблоне имя одно: [| λx → … |]. Как видно на этом примере, вложенность цитат и вклеек может быть любой, но важно чтобы они чередовались — нельзя цитировать внутри цитаты и вклеивать внутри вклейки.
Такое “квазицитирование” является удобным способом представления Haskell-программ. И оно имеет некоторые ограничения: каждое вхождение переменной связывается с тем значением, которое находится в области видимости, доступной до разворачивания шаблонов. Это правило имеет три случая:
  1. Цитирующие скобки запрещают “захват” локальных переменных, используемых в одной цитате из другой (так же как в обычной Haskell-программе нельзя использовать переменные замыкания вне его). Это объясняется, упомянутой выше, автоматической унификацией идентификаторов. Только цитата [p| … |] не переименовывает те переменные, которые вводит генерируемый образец (поскольку с этими переменными будет происходить связывание при сопоставлении с образцом и если у них будут новые произвольные имена, не понятно, как к ним обращаться).
  2. Глобальные идентификаторы, используемые в цитате, “захватывают” все необходимые идентификаторы доступные в той среде, где определена цитата (снова, как в обычном Haskell), поэтому значение цитаты можно без проблем использовать в других модулях, которые не имеют доступа ко всем этим внутренним определениям или даже имеют свои определения для тех же идентификаторов. Это правило использует внутренний механизм GHC ссылок на символы из других модулей (то есть квалифицирует их). Например, цитата [| map |] будет преобразована в “GHC.Base.map”, а цитата типа [t| [String] → Bool |] преобразуется в “[GHC.Base.String] → GHC.Bool.Bool”. Если вам нужны идентификаторы именно из той области видимости, в которой будет вклеиваться шаблон, используйте для них обёртку $(dyn "…"). При этом следует понимать, что так можно случайно использовать идентификатор, определённый локально кем-то другим и возникнет конфликт или шаблон сгенерирует не тот код, который ожидается, поэтому в документации к dyn написано, что это не гигиеничная функция.
  3. Также, внутри цитирующих скобок можно использовать локальные переменные функции. На стадии компиляции это переменные (просто связанные идентификаторы), но во время исполнения кода, — это просто константы, поэтому TH просто подставит на их места соответствующие значения. Так, выражение let x = 5 in [| … x … |] будет преобразовано в let x = 5 in [| … $(lift x) … |]то есть не нужно вручную оборачивать идентификатор локальной переменной в тип Q Exp.

Вклейка и цитирование — это взаимно обратные операции: одна преобразует структуру Exp в Haskell-код, а другая — Haskell-код в структуру Exp, поэтому они взаимно аннигилируются:
$( [| выражение |] ) ≡ выражение
[| $( структура ) |] ≡ структура

Это позволяет, мыслить только в терминах генерируемого Haskell-кода, разрабатывая TH-программы, и не думать о внутренних структурах, представляющих его синтаксис.
Рассмотрим для примера вычисление вклейки “$(summ 3)”. Просто будем заменять использование шаблона на его определение:
   $(summ 3)
  $(summ' 3 [| 0 |])
  $([| λx → $(summ' (3-1) [| $([| 0 |]) + x |]) |])

Теперь мы можем убрать лишние скобки $([| … |]), заменяя по ходу дела “x”, на уникальный идентификатор:
  λx1 → $(summ' (3-1) [| 0 + x1 |])

Снова подставляем определение summ':
  λx1 → $([| λx → $(summ' (2-1) [| $([| 0 + x1 |]) + x |]) |])

Далее будем повторять последние два шага, пока это возможно:
  λx1 → λx2 → $(          summ' (2-1)      [| 0 + x1 + x2 |]             )
  λx1 → λx2 → $([| λx → $(summ' (1-1) [| $([| 0 + x1 + x2 |]) + x  |]) |])
  λx1 → λx2 →     λx3 → $(summ' (1-1) [|      0 + x1 + x2     + x3 |])
  λx1 → λx2 →     λx3 → $(            [|      0 + x1 + x2     + x3 |])
  λx1 → λx2 →     λx3 →                       0 + x1 + x2     + x3

Интересно, что это в этом определении левая часть лямбда-выражения (λx1 → λx2 → …) рекурсивно строится по ходу разворачивания рекурсии, а правая часть (0 + x1 + …) в то же время аккумулируется в оставшейся части шаблона. Такая же техника используется в примере шаблона printf.

Пример: printf


Теперь мы разберём определение шаблона printf, который упоминался в первой части статьи. Далее приводится код с пояснениями, а также модуль Main, использующий его. Скомпилировать его можно командой ghc -XTemplateHaskell --make Main.hs
  • Main.hs
    {-# LANGUAGE TemplateHaskell #-}
    
    module Main where
     
    -- Импортируем наш шаблон printf
    import Printf (printf)
     
    -- Оператор $( … ) развернёт шаблон с данным параметром
    -- в обычный Haskell-код во время компиляции и
    -- вклеит его на то же место – как аргумент putStrLn
    main = putStrLn ( $(printf "Error in file %s line %d: %s") "io.cpp" 325 "printer not found" )
    


    Printf.hs
    {-# LANGUAGE TemplateHaskell #-}
    
    module Printf where
     
    -- Импортируем инструментарий Template Haskell
    import Language.Haskell.TH
     
    -- Описание строки форматирования
    data Format = D            -- представляет "%d" – подстановка чисел
                | S            -- представляет "%s" – подстановка строк
                | L String     -- представляет остальной текст (L от Literally)
     
    -- Парсер строки форматирования – преобразовывает её в структуру Format
    parse :: String -> String -> [Format]
    parse          ""  rest  = [L rest]
    parse ('%':'d':xs) rest  =  L rest : D : parse xs ""
    parse ('%':'s':xs) rest  =  L rest : S : parse xs ""
    parse       (x:xs) rest  =  parse xs (rest++[x])
     
    -- Генератор Haskell-кода, подставляющий вместо элементов
    -- форматирования соответствующие лямбда-выражения
    gen :: [Format] -> ExpQ -> ExpQ
    gen        []  code = code
    gen (D   : xs) code = [| \x -> $(gen xs [| $code ++ show x |]) |]
    gen (S   : xs) code = [| \x -> $(gen xs [| $code ++ x |]) |]
    gen (L s : xs) code = gen xs [| $code ++ s |]
     
    -- Шаблон, который берёт на вход строку форматирования
    -- парсит её и генерирует соответствующий код
    printf :: String -> ExpQ
    printf s = gen (parse s "") [| "" |]
    


    Это авторский код с переведёнными комментариями. Аналогичное, но более короткое (и более простое, по-моему) решение можно посмотреть тут: Gist.

    Другие примеры


    Я немного экспериментировал с цитированием объявлений функций и вклейкой имён – эти эксперименты описаны в моём блоге. Буду рад советам и рекомендациям.
Теги:
Хабы:
Всего голосов 17: ↑17 и ↓0+17
Комментарии2

Публикации