Spec-Zone.ru › Haskell 9

6.13. Template Haskell

Template Haskell позволяет выполнять метапрограммирование на Haskell во время компиляции. Предпосылки основных технических новшеств рассматриваются в статье «Метапрограммирование с шаблонами для Haskell» (Proc Haskell Workshop 2002). Первый пример из этой статьи приведён ниже (Практический пример использования Template Haskell), чтобы помочь вам начать работу.

6.13.1. Синтаксис

TemplateHaskell
Включает:

TemplateHaskellQuotes

Начиная с версии:

6.0. Типизированные сплайсы появились в GHC 7.8.1.

Включает синтаксис сплайсов и цитирования Template Haskell.

TemplateHaskellQuotes
Начиная с версии:

8.0.1

Включает синтаксис цитирования Template Haskell.

В Template Haskell появились следующие синтаксические конструкции. Чтобы включить эти синтаксические расширения, необходимо использовать расширение TemplateHaskell. Также можно использовать расширение TemplateHaskellQuotes, чтобы включить подмножество Template Haskell, связанное с цитированием (то есть без сплайсов верхнего уровня).

  • Цитирование выражения записывается в угловых скобках Оксфорда, например:

    • [| ... |] или [e| ... |], где «…» — это выражение; цитата имеет тип Quote m => m Exp.
    • [d| ... |], где «…» — это список объявлений верхнего уровня; цитата имеет тип Quote m => m [Dec].
    • [t| ... |], где «…» — это тип; цитата имеет тип Quote m => m Type.
    • [p| ... |], где «…» — это шаблон; цитата имеет тип Quote m => m Pat.

    Класс типов Quote (Language.Haskell.TH.Syntax.Quote) — это минимальный интерфейс, необходимый для реализации десахаризации цитат. Монада Q является экземпляром Quote, но содержит гораздо больше операций, не требуемых для определения цитат.

    Информацию об использовании частичных сигнатур типов в цитатах см. в разделе Где они могут встречаться?.

  • Сплайс записывается как $x, где x — произвольное выражение. Между «$» и выражением не должно быть пробела.

    Сплайс верхнего уровня может использоваться вместо

    • выражения; подставляемое выражение должно иметь тип Q Exp
    • шаблона; подставляемый шаблон должен иметь тип Q Pat
    • типа; подставляемое выражение должно иметь тип Q Type
    • списка объявлений верхнего уровня; подставляемое выражение должно иметь тип Q [Dec]

    Обратите внимание, что сплайсы объявлений разрешены только на верхнем уровне (вне любых других объявлений).

    Монада Q определена в Language.Haskell.TH.Syntax и поддерживает ряд полезных операций, применяемых при генерации кода, например вывод сообщений об ошибках или поиск идентификаторов в окружении.

    В этом случае $ переопределяет значение инфиксного оператора, подобно тому как M.x переопределяет значение . как инфиксного оператора. Чтобы использовать инфиксный оператор, поставьте вокруг него пробелы.

  • Сплайсы можно вкладывать в скобки цитирования. Например, фрагмент, представляющий 1 + 2, можно построить с помощью вложенных сплайсов:

    oneC, twoC, plusC  :: Quote m => m Exp
    oneC = [| 1 |]
    
    twoC = [| 2 |]
    
    plusC = [| $oneC + $twoC |]
    
  • Цитирование типизированного выражения записывается как [|| ... ||] или [e|| ... ||], где «…» — это выражение; если выражение «…» имеет тип a, то цитата имеет тип Quote m => Code m a.

    Извлечь значение типа m Exp из Code m a можно с помощью функции unTypeCode :: Code m a -> m Exp.

  • Сплайс типизированного выражения записывается как $$x, где x — произвольное выражение.

    Типизированный сплайс выражения верхнего уровня может использоваться вместо выражения; подставляемое выражение должно иметь тип Code Q a

    ПРИМЕЧАНИЕ: В настоящее время типизированные сплайсы могут подавлять предупреждение о неиспользуемом идентификаторе для идентификаторов в области видимости. См. #16524.

  • Квазицитирование может встречаться в контексте шаблона, типа, выражения или объявления и также записывается в угловых скобках Оксфорда:

    • [varid| ... |], где «…» — произвольная строка; полное описание механизма квазицитирования приведено в разделе Квазицитирование в Template Haskell.
  • Имя можно цитировать, используя одну или две начальные одинарные кавычки:

    • 'f имеет тип Name и обозначает функцию f. Аналогично, 'C имеет тип Name и обозначает конструктор данных C. В общем случае '⟨thing⟩ интерпретирует ⟨thing⟩ в контексте выражения.

      Имя, второй символ которого — одинарная кавычка, нельзя цитировать именно таким способом, поскольку вместо этого оно будет разобрано как символ в кавычках. Например, если функция называется f'7 (что является допустимым идентификатором Haskell), попытка процитировать её как 'f'7 будет разобрана как символьный литерал 'f', за которым следует числовой литерал 7. Как и в случае продвинутых конструкторов (Как различать типы и конструкторы), эту проблему можно обойти, добавив пробел между кавычкой и именем. Поэтому имя функции f'7 записывается как ' f'7.

    • ''T имеет тип Name и обозначает конструктор типа T. То есть ''⟨thing⟩ интерпретирует ⟨thing⟩ в контексте типа.

    Эти Names можно использовать для построения выражений, шаблонов, объявлений Template Haskell и т. д. Их также можно передавать в качестве аргумента функции reify.

  • Точный тип цитаты зависит от типов вложенных в неё сплайсов:

    -- Add a redundant constraint to demonstrate that constraints on the
    -- monad used to build the representation are propagated when using nested
    -- splices.
    f :: (Quote m, C m) => m Exp
    f = [| 5 | ]
    
    -- f is used in a nested splice so the constraint on f, namely C, is propagated
    -- to a constraint on the whole representation.
    g :: (Quote m, C m) => m Exp
    g = [| $f + $f |]
    

    Помните, что для сплайса верхнего уровня аргумент по-прежнему должен иметь тип Q Exp. Поэтому сплайс g приведёт к тому, что m будет инстанцирован как Q:

    h :: Int
    h = $(g) -- m ~ Q
    

6.13.2. Уровни и этапы

Template Haskell выполняет код как во время компиляции, так и во время выполнения, поэтому необходимо понимать два ключевых понятия: уровни и этапы.

Уровни — это понятие, используемое проверкой типов для обеспечения правильного разделения кода по этапам, то есть возможности компилятора выполнять операции времени компиляции до операций времени выполнения. Этапы — это фактические моменты компиляции и выполнения кода. Уровни — семантическое понятие, используемое проверкой типов, тогда как этапы — понятие операционное, свойство вычисления.

6.13.2.1. Понимание уровней

Каждое выражение в программе существует на определённом целочисленном уровне:

  • Уровень 0: обычные объявления верхнего уровня в модуле
  • Уровень -1: код внутри сплайса верхнего уровня (код, выполняемый во время компиляции)
  • Уровень 1: код внутри цитаты (код, цитируемый для выполнения во время работы программы)

При входе в цитаты и сплайсы уровень меняется:

  • Внутри цитаты [| e |] уровень повышается на 1
  • Внутри сплайса $( e ) уровень понижается на 1

Таким образом, уровень можно вычислить как количество окружающих цитат минус количество окружающих сплайсов. Например:

-- foo is at level 0
foo = $(let
         -- bar is at level -1
         bar = $(let
                  -- baz is at level -2
                  baz = [|
                          -- qux is at level -1
                          qux = [|
                                  -- quux is at level 0
                                  quux = [|
                                          -- quuz is at level 1
                                          quuz = 0
                                         |]
                                |]
                        |]
               in baz)
      in bar)

Сплайсы верхнего уровня (определяющие места выполнения кода во время компиляции) отличаются тем, что их тело находится на отрицательном уровне.

  • Объявления верхнего уровня вводят переменные на уровне 1.
  • Импорты вводят переменные на уровне 1.
  • Локальные переменные вводятся на уровне своего выражения. Например, x в [| let x = 0 in … |] находится на уровне 2.

6.13.2.2. Сохранение между этапами

В обычном Template Haskell сохранение между этапами (CSP) позволяет использовать идентификаторы на уровнях, отличных от того, на котором они были определены. Для этого предусмотрено два механизма:

  1. Сохранение на основе пути: оно позволяет использовать глобальное определение на другом уровне в двух случаях:

    • Любой глобальный идентификатор можно использовать на более позднем уровне (то есть внутри цитаты).
    • Импортированный идентификатор можно использовать на более раннем уровне (то есть в сплайсе).

    Расширение ImplicitStagePersistence управляет включением сохранения на основе пути. По умолчанию оно включено во всех текущих редакциях языка.

  2. Сохранение на основе сериализации: оно позволяет использовать локально связанные переменные на более высоких уровнях с помощью класса типов Lift:

    tardy x = [| x |]  -- This is elaborated to [| $(lift x) |]
    

    Когда компилятор обнаруживает ошибку уровня из-за использования переменной на уровень выше того, где она определена, он автоматически вставляет lift, чтобы сериализовать переменную на требуемом уровне.

    Эта возможность доступна пользователю через класс типов Lift в модуле Language.Haskell.TH.Syntax. Если для типа существует экземпляр Lift, любое его значение можно преобразовать в выражение Template Haskell:

    class Lift t where
        lift :: Quote m => t -> m Exp
        liftTyped :: Quote m => t -> Code m t
    

    Экземпляры Lift определены для большинства встроенных типов; их также можно вывести с помощью расширения DeriveLift. Дополнительную информацию см. в разделе Выведение экземпляров Lift.

Сохранение на основе пути объясняет, почему работает следующий код:

module M where

suc :: Int -> Int
suc = (+1)

one :: Q Exp
one = [| \x -> suc x |]  -- suc is used at level 1, defined at level 0

two = $(one)  -- one is used at level -1, defined at level 0

При использовании расширений ExplicitLevelImports и NoImplicitStagePersistence сохранение на основе пути отключается, поэтому требуется явно указывать, какие идентификаторы можно использовать на каких уровнях.

6.13.2.3. Этапы и компиляция

Если уровни — это понятие проверки типов, то этапы обозначают фактические моменты компиляции и выполнения модулей:

  • Этап C (время компиляции): код, выполняемый во время компиляции
  • Этап R (время выполнения): код, выполняемый при запуске скомпилированной программы

Компилятору может потребоваться компилировать код по-разному в зависимости от этапа. Например, если используется -fno-code, для этапа R код не нужен, но для этапа C потребуется генерация кода. Если компилятор динамически скомпонован, код этапа C потребуется компоновать динамически, тогда как код этапа R можно скомпоновать статически.

Правила сохранения между этапами, допускаемые языком, зависят от предположений о структуре этапов. Для GHC с расширением ImplicitStagePersistence необходимо предполагать, что модуль будет доступен на всех этапах. Это строгое требование.

6.13.3. Группы объявлений

Сплайсы объявлений верхнего уровня разбивают исходный файл на группы объявлений. Группа объявлений — это группа объявлений, созданная сплайсом объявлений верхнего уровня, вместе со следующими за ним объявлениями вплоть до следующего сплайса объявлений верхнего уровня, не включая его. Обратите внимание: границы групп объявлений задают только сплайсы верхнего уровня, но не сплайсы выражений. Первая группа объявлений в модуле включает все объявления верхнего уровня вплоть до первого сплайса объявлений верхнего уровня, не включая его.

Каждая группа компилируется так же, как отдельно компилируемый модуль. Это означает, что:

  • В более поздних группах видны объявления и объявления экземпляров из предыдущих групп;
  • Но в предыдущих группах не видны объявления и объявления экземпляров из последующих групп.

Взаимная рекурсия возможна только внутри одной группы объявлений. Группы объявлений могут обращаться к определениям из предыдущих групп, но не из последующих.

Соответственно, окружение типов, видимое из reify, включает все объявления верхнего уровня вплоть до конца непосредственно предшествующей группы объявлений, но не более того.

В отличие от обычных сплайсов объявлений, квазицитирователи объявлений не создают границу. Такие квазицитирователи раскрываются до обработки остальной части группы объявлений, а созданные ими объявления объединяются с окружающей группой объявлений. Поэтому окружение типов, видимое из reify в квазицитирователе объявлений, не будет включать ничего из группы объявлений этого квазицитирователя.

Рассмотрим следующий код:

module M where

import ...

f x = x

$(th1 4)

h y = k y y $(blah1)

[qq|blah|]

k x y z = x + y + z

$(th2 10)

w z = $(blah2)

В этом примере reify внутри…

  1. Сплайс $(th1 ...) увидит определение f — сплайс находится на верхнем уровне, поэтому видны все определения из предыдущей группы объявлений (то есть все определения модуля вплоть до самого сплайса, не включая его).
  2. Сплайс $(blah1) не может обращаться к функции w — w находится в последующей группе объявлений и потому невидим. Аналогично, $(blah1) не увидит определение h, поскольку оно находится в той же группе объявлений, что и $(blah1). Однако сплайс $(blah1) увидит определение f, поскольку оно находится в непосредственно предшествующей группе объявлений.
  3. Сплайс $(th2 ...) увидит определение f, все связывания, созданные $(th1 ...), определение h и все связывания, созданные [qq|blah|] (все они находятся в предыдущих группах объявлений).
  4. Тело h может обращаться к функции k, расположенной по другую сторону квазицитирователя объявлений, поскольку квазицитирователи не разбивают группу объявлений.
  5. Квазицитирователь qq сможет видеть определение f из предшествующей группы объявлений, но не определения h или k и не какие-либо определения из последующих групп объявлений.
  6. Сплайс $(blah2) увидит те же определения, что и сплайс $(th2 ...) (но не связывания, которые он создаёт).

Обратите внимание: поскольку сплайс выражения не может обращаться к объявлениям из той же группы объявлений, для разделения группы можно добавить пустой сплайс верхнего уровня:

module M where

data D = C1 | C2

f1 = $(th1 ...)

$(return [])

f2 = $(th2 ...)

Здесь

  1. Сплайс $(th1 ...) не может обращаться к D — оно находится в той же группе объявлений.
  2. Группа объявлений, содержащая D, завершается пустым сплайсом объявлений верхнего уровня $(return []) (помните, Q — это монада, поэтому достаточно просто выполнить return для пустого списка объявлений).
  3. Поскольку группа объявлений, содержащая D, теперь является предыдущей, сплайс $(th2 ...) может обращаться к D.

Обратите внимание: в некоторых случаях наличие или отсутствие сплайсов объявлений верхнего уровня может влиять на поведение окружающего кода во время выполнения, поскольку разрешение экземпляров может различаться в зависимости от их видимости. Один из примеров — несогласованные экземпляры.

module Main where

main :: IO ()
main = do
  let i :: Int
      i = 42
  putStrLn (m1 i)
  putStrLn (m2 i)

class C1 a where
  m1 :: a -> String

instance {-# INCOHERENT #-} C1 a where
  m1 _ = "C1 incoherent"

instance C1 Int where
  m1 = show

class C2 a where
  m2 :: a -> String

instance {-# INCOHERENT #-} C2 a where
  m2 _ = "C2 incoherent"

$(return [])

instance C2 Int where
  m2 = show

Здесь C1 и C2 — одни и те же классы с почти идентичными экземплярами. Единственное существенное различие между C1 и C2, помимо небольшого изменения имени, состоит в том, что все экземпляры C1 определены в одной группе объявлений, тогда как экземпляр C2 Int помещён в отдельную группу объявлений, отличную от группы несогласованного экземпляра C2 a. Это влияет на поведение функции main во время выполнения:

$ runghc Main.hs
42
C2 incoherent

Обратите внимание, что m1 i возвращает "42", а m2 i возвращает "C2 incoherent". При проверке типов каждого из этих выражений GHC должен определить, какие экземпляры C1 Int и C2 Int использовать:

  1. При поиске экземпляра C1 Int GHC обнаруживает в одной группе объявлений два возможных экземпляра: несогласованный экземпляр C1 a и согласованный экземпляр C1 Int. Согласно правилам поиска экземпляров, описанным в разделе Перекрывающиеся экземпляры, поскольку для выбора имеется ровно один согласованный экземпляр, GHC выберет экземпляр C1 Int. В результате m1 i будет эквивалентен show i (то есть "42").
  2. При поиске экземпляра C2 Int GHC обнаруживает в той же группе объявлений только один экземпляр: несогласованный экземпляр C2 a. Обратите внимание, что GHC не видит экземпляр C2 Int, поскольку тот находится в последующей группе объявлений, отделённой промежуточным сплайсом объявлений. В результате GHC выберет экземпляр C2 a, и m2 i станет эквивалентен "C2 incoherent".

6.13.3.1. Прочие особенности

В этом разделе рассматриваются другие особенности и проблемы Template Haskell.

  • В сплайсе объявлений верхнего уровня можно опустить $(...). Достаточно записать выражение (а не объявление), чтобы оно считалось сплайсом. Например, можно написать:

    module Foo where
    import Bar
    
    f x = x
    
    $(deriveStuff 'f)   -- Uses the $(...) notation
    
    g y = y+1
    
    deriveStuff 'g      -- Omits the $(...)
    
    h z = z-1
    

    Это сокращение делает сплайсы объявлений верхнего уровня менее многословными и пугающими.

  • Сплайсы шаблонов вводят переменные-связки, но проверка области видимости переменных в выражениях внутри области видимости шаблона выполняется только при запуске сплайса. Обратите внимание, что сплайсы шаблонов вне скобок цитирования запускаются во время компиляции. Сплайсы шаблонов внутри скобок цитирования не запускаются во время компиляции; они запускаются позднее, когда раскрывается цитата. Например:

    mkPat :: Quote m => m Pat
    mkPat = [p| (x, y) |]
    
    -- in another module:
    foo :: (Char, String) -> String
    foo $(mkPat) = x : z
    
    bar :: Quote m => m Exp
    bar = [| \ $(mkPat) -> x : w |]
    

    приведёт к ошибке, поскольку z недоступен в определении foo, но ошибки из-за недоступности w в определении bar не будет. Она возникнет только при раскрытии bar.

  • Квазицитирователь шаблонов может создавать связывания, область видимости которых распространяется на правую часть определения, поскольку лексически эти связывания находятся в области видимости. Например, если квазицитирователь haskell разбирает Haskell, то в следующем коде y в правой части f ссылается на y, связанный квазицитирователем шаблонов haskell, а не на y = 7 верхнего уровня.

    y :: Int
    y = 7
    
    f :: Int -> Int -> Int
    f n = \ [haskell|y|] -> y+n
    
  • Расширение TemplateHaskellQuotes считается безопасным в рамках Safe Haskell, тогда как расширение TemplateHaskell — нет.
  • Цитаты выражений поддерживают большинство конструкций языка Haskell. Однако в настоящее время цитаты выражений не поддерживают некоторые расширения GHC, в том числе:

    • Отверстия типов в типизированных сплайсах (см. #10945 и #10946).

(По сравнению с исходной статьёй имеется много небольших различий. В синтаксисе сплайса объявления используется «$», а не «splice». Тип заключённого выражения должен быть Quote m => m [Dec], а не [Q Dec]. Типизированные сплайсы и цитаты выражений поддерживаются.)

-fenable-th-splice-warnings

Для сплайсов Template Haskell предупреждения не проверяются, поскольку код, вызывающий предупреждение, может поступать из сторонней библиотеки и, возможно, не был написан пользователем. Чтобы всё же включить предупреждения для сплайсов, передайте -fenable-th-splice-warnings.

6.13.4. Явный импорт уровней

Расширение ExplicitLevelImports вместе с ImplicitStagePersistence даёт программистам детальный контроль над тем, какие модули нужны на каждом этапе выполнения.

Подробное описание расширения см. в статье Explicit Level Imports.

ExplicitLevelImports
Включает:

NoImplicitStagePersistence

Начиная с версии:

9.14.1

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

Добавляются модификаторы импорта splice и quote, позволяющие точно указать уровень идентификаторов, вводимых импортом.

ImplicitStagePersistence
По умолчанию:

включено

Начиная с версии:

9.14.1

Позволяет использовать идентификаторы на уровнях, отличных от уровня их определения, с помощью сохранения на основе путей.

6.13.4.1. Синтаксис и использование

ExplicitLevelImports добавляет два новых модификатора импорта:

  • import splice M (...) — импортирует идентификаторы на уровне -1 (для использования в сплайсах)
  • import quote M (...) — импортирует идентификаторы на уровне 1 (для использования в цитатах)
  • import M (...) — импортирует идентификаторы на уровне 0 (обычный код)

Синтаксис поддерживает оба варианта размещения ключевых слов уровня:

import splice M          -- before the module name
import M splice          -- after the module name
import splice qualified M as MB -- with qualified
import splice M qualified as MB -- with -XImportQualifiedPost
import M splice qualified as MB -- with -XImportQualifiedPost

6.13.4.2. Простые примеры

Явный импорт уровней позволяет точнее указывать, какие модули нужны на каждом уровне.

{-# LANGUAGE TemplateHaskell #-}
module Main where

import Control.Lens.TH (makeLenses)
import OtherModule (someFunction)

data User = User { _name :: String, _age :: Int }

$(makeLenses ''User)

main = print (someFunction (User "John" 30))

В этой версии и Control.Lens.TH, и OtherModule импортируются обычным способом. GHC должен скомпилировать оба модуля, прежде чем сможет начать проверку типов Main, поскольку заранее неизвестно, какие импорты могут понадобиться при вычислении сплайса makeLenses. Хотя в сплайсе фактически используется только makeLenses, GHC должен предполагать, что может понадобиться любой импортированный идентификатор.

Если использовать ExplicitLevelImports, можно точнее указать, какие модули нужны на каждом уровне. Например:

.. code-block:: haskell

{-# LANGUAGE TemplateHaskell, ExplicitLevelImports #-} module Main where

import splice Control.Lens.TH (makeLenses) import OtherModule (someFunction)

data User = User { _name :: String, _age :: Int }

$(makeLenses ‘’User)

main = print (someFunction (User “John” 30))

При явном импорте уровней мы пометили Control.Lens.TH ключевым словом splice, сообщив GHC, что этот модуль необходим во время компиляции для вычисления сплайсов. Это даёт GHC важную информацию:

  1. Control.Lens.TH необходимо скомпилировать в объектный код до проверки типов Main
  2. Для OtherModule достаточно проверки типов до Main, тогда как генерация кода может выполняться параллельно
  3. Control.Lens.TH не понадобится во время выполнения (если на него нет других ссылок)

Такое разграничение даёт несколько преимуществ:

  • GHC не нужно дожидаться полной компиляции OtherModule, прежде чем начать работу над Main
  • Control.Lens.TH не будет включён в итоговый исполняемый файл, поскольку нужен только во время компиляции
  • Структура этапов программы становится более явной

Ещё один пример, показывающий разные уровни импорта:

{-# LANGUAGE TemplateHaskell, ExplicitLevelImports #-}
module Advanced where

import splice A (makeFunction)   -- Used in splices (level -1)
import B (normalFunction)        -- Used in normal code (level 0)
import quote C (runtimeValue)    -- Used in quotes (level 1)

-- This generates a function at compile time
$(makeFunction "generatedFunction")

-- This uses a normal function at runtime
result = normalFunction 42

-- This creates a quotation containing code that will use runtimeValue
quotation = [| runtimeValue * 2 |]

В этом примере мы явно указываем целевой уровень для каждого импорта: * A предоставляет код, выполняемый во время компиляции (в сплайсах) * B предоставляет код, выполняемый при обычном запуске программы * C предоставляет значения, на которые будут ссылаться в цитируемом коде

6.13.4.3. Правила уровней и ошибки

При использовании NoImplicitStagePersistence:

  • Функции, импортированные на уровне 0, можно использовать только на уровне 0
  • Функции, импортированные с помощью splice, можно использовать только внутри сплайсов верхнего уровня
  • Функции, импортированные с помощью quote, можно использовать только внутри цитат

Если использовать идентификатор не на том уровне, возникнет ошибка:

import splice A (foo)       -- foo at level -1
import B (bar)              -- bar at level 0
import quote C (baz)        -- baz at level 1

x = $(foo 42)               -- OK: foo used at level -1
y = $(bar 42)               -- Error: bar imported at level 0 but used at level -1
z = [| baz 42 |]            -- OK: baz used at level 1
w = [| bar 42 |]            -- Error: bar imported at level 0 but used at level 1

6.13.4.4. Экземпляры классов и уровни

На экземпляры классов также распространяется проверка уровней. Экземпляры должны быть доступны на уровне, где они используются:

  • Экземпляры из текущего модуля находятся на уровне 0
  • Экземпляры из модулей, импортированных обычным способом, находятся на уровне 0
  • Экземпляры из модулей, импортированных с помощью splice, находятся на уровне -1
  • Экземпляры из модулей, импортированных с помощью quote, находятся на уровне 1

Поскольку классы импортируются транзитивно, проверка типов гарантирует наличие корректно размеченного по уровням пути для доступа к любому экземпляру. Например, если экземпляр нужен на уровне -1, он должен входить в транзитивное замыкание модулей, импортированных с помощью splice.

6.13.4.5. Импорт Prelude

Неявный импорт Prelude вводит идентификаторы в область видимости только на уровне 0. Если функции Prelude нужны в сплайсах или цитатах, их необходимо импортировать явно:

import splice Prelude (map, filter)  -- Use these in splices
import quote Prelude (show, (+))     -- Use these in quotes

6.13.4.6. Примечания и ограничения

  • Локальные определения (определённые в том же модуле) по-прежнему подчиняются правилам уровней — функцию нельзя использовать в сплайсе, если она определена в том же модуле
  • ExplicitLevelImports особенно полезно, когда большая часть кода Template Haskell сосредоточена в нескольких модулях
  • Определение экземпляров Lift требует особой обработки, поскольку тип данных должен быть доступен и во время компиляции, и во время выполнения

6.13.5. Использование Template Haskell

  • Типы данных и функции-конструкторы с монадическим интерфейсом для Template Haskell находятся в библиотеке Language.Haskell.TH.Syntax.
  • Функцию можно выполнить во время компиляции, только если она импортирована из другого модуля, не входящего в группу взаимно рекурсивных модулей, в которую входит компилируемый в данный момент модуль. Кроме того, все модули этой группы взаимной рекурсии должны быть достижимы из модуля, в котором запускается сплайс, по импортам, не являющимся SOURCE-импортами.

    Например, при компиляции модуля A можно выполнять функции Template Haskell, импортированные из B, только если B не импортирует A (прямо или косвенно). Причина очевидна: чтобы выполнить B, необходимо скомпилировать и выполнить A, но в этот момент мы проверяем типы A.

  • Если вы собираете GHC из исходного кода, для выполнения сплайсов и квазицитат Template Haskell требуется как минимум загрузочный компилятор stage-2. Компилятор stage-1 принимает только обычные цитаты Haskell. Причина в том, что сплайсы и квазицитаты TH компилируют и выполняют программу, а затем анализируют результат. Поэтому важно, чтобы скомпилированная программа выдавала результаты, представления которых идентичны представлениям самого компилятора.

6.13.6. Просмотр кода, сгенерированного Template Haskell

Флаг -ddump-splices показывает раскрытие всех сплайсов объявлений верхнего уровня — типизированных и нетипизированных — по мере их обработки. Как и для всех флагов дампа, по умолчанию вывод направляется в stdout. Для нетривиальной программы может быть полезно использовать этот флаг вместе с флагом -ddump-to-file (см. Вывод промежуточных структур компилятора). Для каждого файла, использующего Template Haskell, вывод будет записан в файл .dump-splices.

Флаг -dth-dec-file записывает раскрытия всех сплайсов объявлений TH верхнего уровня — типизированных и нетипизированных — в файл M.th.hs для каждого компилируемого модуля M. Обратите внимание, что другие виды сплайсов (выражения, типы и шаблоны) не отображаются. Разработчики приложений могут добавить этот файл в репозиторий, чтобы искать в нём идентификаторы, определённые с помощью Template Haskell. Это похоже на использование -ddump-to-file вместе с -ddump-splices, но файл создаётся всегда, а не только при использовании -ddump-to-file. Формат также отличается: исходный код файла не отображается, выводится только сгенерированный код, а место сплайса в исходном файле отмечено комментарием.

Ниже приведён пример вывода флага -ddump-splices.

TH_pragma.hs:(6,4)-(8,26): Splicing declarations
  [d| foo :: Int -> Int
      foo x = x + 1 |]
======>
  foo :: Int -> Int
  foo x = (x + 1)

Ниже приведён вывод того же примера при использовании флага -dth-dec-file.

-- TH_pragma.hs:(6,4)-(8,26): Splicing declarations
foo :: Int -> Int
foo x = (x + 1)

6.13.7. Практический пример Template Haskell

Чтобы преодолеть неуверенность, попробуйте этот упрощённый практический пример. Скопируйте и вставьте два приведённых ниже модуля в Main.hs и Printf.hs:

{- Main.hs -}
module Main where

-- Import our template "pr"
import Printf ( pr )

-- The splice operator $ takes the Haskell source code
-- generated at compile time by "pr" and splices it into
-- the argument of "putStrLn".
main = putStrLn ( $(pr "Hello") )


{- Printf.hs -}
module Printf where

-- Skeletal printf from the paper.
-- It needs to be in a separate module to the one where
-- you intend to use it.

-- Import some Template Haskell syntax
import Language.Haskell.TH

-- Describe a format string
data Format = D | S | L String

-- Parse a format string.  This is left largely to you
-- as we are here interested in building our first ever
-- Template Haskell program and not in building printf.
parse :: String -> [Format]
parse s   = [ L s ]

-- Generate Haskell source code from a parsed representation
-- of the format string.  This code will be spliced into
-- the module which calls "pr", at compile time.
gen :: Quote m => [Format] -> m Exp
gen [D]   = [| \n -> show n |]
gen [S]   = [| \s -> s |]
gen [L s] = stringE s

-- Here we generate the Haskell code for the splice
-- from an input format string.
pr :: Quote m => String -> m Exp
pr s = gen (parse s)

Теперь запустите компилятор:

$ ghc --make -XTemplateHaskell main.hs -o main

Запустите main — и получите следующий вывод:

$ ./main
Hello

6.13.8. Цитаты Template Haskell и переназначаемый синтаксис

Переназначаемый синтаксис плохо сочетается с нетипизированными цитатами TH: применение правил переназначаемого синтаксиса противоречило бы свободным ограничениям нетипизированных цитат, которые допустимы даже при наличии несвязанных идентификаторов (см. #18102). Применение к ним правил переназначаемого синтаксиса потребовало бы, чтобы в области видимости кода, определяющего эти цитаты, находились все необходимые функции (например, ifThenElse или fromInteger), вместо того чтобы отложить разрешение этих символов до кода, который вставляет цитируемый синтаксис Haskell, как обычно происходит с нетипизированным TH. Поэтому, даже если в модуле есть нетипизированные цитаты TH и включено RebindableSyntax, GHC отключает переназначаемый синтаксис при обработке цитат. Однако код, вставляющий цитаты, может включить RebindableSyntax, чтобы к полученному коду применялись обычные правила.

Типизированные цитаты TH, напротив, полностью совместимы с немедленным применением правил переназначаемого синтаксиса. Поэтому GHC обработает такие цитаты согласно этим правилам, если в модулях, где они встречаются, включено расширение RebindableSyntax.

6.13.9. Использование Template Haskell с профилированием

Template Haskell использует встроенные в GHC компилятор и интерпретатор байт-кода для выполнения выражений сплайсов. Интерпретатор байт-кода выполняет скомпилированное выражение в той же среде выполнения, в которой работает сам GHC. Это означает, что код, на который ссылается интерпретируемое выражение, должен быть совместим с этой средой выполнения. В частности, объектный код, скомпилированный для профилирования, нельзя загрузить и использовать в выражении сплайса, поскольку профилируемый объектный код совместим только с версией среды выполнения для профилирования.

Это создаёт трудности, если программа из нескольких модулей содержит код Template Haskell и её необходимо скомпилировать для профилирования: GHC не может загрузить профилируемый объектный код и использовать его при выполнении сплайсов.

К счастью, GHC предлагает два обходных решения.

Первый вариант — скомпилировать программу дважды:

  1. Сначала скомпилируйте программу или библиотеку обычным способом, без -prof.
  2. Затем скомпилируйте её ещё раз с -prof и дополнительно используйте -osuf p_o, чтобы задать для объектных файлов другие имена (здесь можно выбрать любой суффикс, отличный от обычного суффикса объектных файлов). При выполнении выражений сплайсов GHC автоматически загрузит объектные файлы, созданные на первом шаге. Если при сборке с -prof и использовании Template Haskell не указать флаг -osuf ⟨suffix⟩, GHC выдаст сообщение об ошибке.

Второй вариант — добавить флаг -fexternal-interpreter (см. Запуск интерпретатора в отдельном процессе). Интерпретатор будет запущен в отдельном процессе, где сможет напрямую загружать и выполнять профилируемый код. Повторно компилировать код не нужно — просто добавьте -fexternal-interpreter, и всё должно работать. (В GHC 8.0.x этот вариант является экспериментальным, но в будущих выпусках он может стать вариантом по умолчанию.)

6.13.10. Квазицитирование Template Haskell

QuasiQuotes
Начиная с версии:

6.10.1

Включает синтаксис квазицитирования Template Haskell.

Квазицитирование позволяет записывать шаблоны и выражения с помощью конкретного синтаксиса, определяемого программистом. Причины появления этого расширения и несколько примеров описаны в статье «Why It’s Nice to be Quoted: Quasiquoting for Haskell» (Proc Haskell Workshop 2007). В примере ниже показано, как написать квазицитатор для простого языка выражений.

Основные особенности:

  • Квазицитата имеет вид [quoter| string |].

    • ⟨quoter⟩ должно быть именем импортированного квазицитатора, квалифицированным или неквалифицированным; произвольное выражение использовать нельзя.
    • ⟨quoter⟩ не может быть «e», «t», «d» или «p», поскольку они пересекаются с цитатами Template Haskell.
    • В токене [quoter| не должно быть пробелов.
    • Цитируемая ⟨string⟩ может быть произвольной и содержать символы новой строки.
    • Цитируемая ⟨string⟩ заканчивается при первом вхождении двухсимвольной последовательности "|]". Экранирование не выполняется. Чтобы включить эту последовательность символов в строку, необходимо определить собственное соглашение об экранировании (например, использовать строку "|~]") и предусмотреть в функции-квазицитаторе преобразование "|~]" в "|]". Один из способов реализации — объединить квазицитатор с предварительной обработкой, выполняющей такое преобразование. Подробности см. в обсуждении #5348.
  • Квазицитата может использоваться вместо:

    • Выражения
    • Шаблона
    • Типа
    • Объявления верхнего уровня

    (В статье описаны только первые два варианта.)

  • Квазицитатор — это значение типа Language.Haskell.TH.Quote.QuasiQuoter, определённого следующим образом:

    data QuasiQuoter = QuasiQuoter { quoteExp  :: String -> Q Exp,
                                     quotePat  :: String -> Q Pat,
                                     quoteType :: String -> Q Type,
                                     quoteDec  :: String -> Q [Dec] }
    

    Иными словами, квазицитатор — это кортеж из четырёх парсеров, по одному для каждого контекста, в котором может встречаться квазицитата.

  • Квазицитата раскрывается путём применения соответствующего парсера к строке, заключённой в скобки Оксфорда. Контекст квазицитаты (выражение, шаблон, тип или объявление) определяет, какой парсер будет вызван.
  • В отличие от обычных сплайсов объявлений вида $(...), квазицитаты объявлений не приводят к разрыву группы объявлений. Дополнительные сведения см. в разделе Синтаксис.

Предупреждение

QuasiQuotes приводит к нежелательной неоднозначности с синтаксисом списковых включений. Рассмотрим следующий пример:

let x = [v| v <- [0..10]]

Без QuasiQuotes это разбирается как списковое включение. С QuasiQuotes это разбирается как квазицитата, однако разбор завершается ошибкой из-за отсутствия закрывающего |]. См. #11679.

Пример ниже демонстрирует квазицитирование. Квазицитатор expr связан со значением типа QuasiQuoter, определённым в модуле Expr. В примере используется аннотированная переменная n, обозначенная синтаксисом 'int:n (этот синтаксис аннотирования определён автором парсера, а не GHC). При сопоставлении с образцом n получает целочисленное значение аргумента конструктора IntExpr. Дополнительные сведения об аннотировании, а также описание метода с использованием SYB, позволяющего на основе одного парсера типа String -> a создать парсер выражений, возвращающий значение типа Q Exp, и парсер шаблонов, возвращающий значение типа Q Pat, см. в упомянутой статье.

На квазицитаторы распространяются те же ограничения уровней, что и на Template Haskell. Например, expr нельзя определить в Main.hs, где он используется: его необходимо импортировать.

{- ------------- file Main.hs --------------- -}
module Main where

import Expr

main :: IO ()
main = do { print $ eval [expr|1 + 2|]
          ; case IntExpr 1 of
              { [expr|'int:n|] -> print n
              ;  _              -> return ()
              }
          }


{- ------------- file Expr.hs --------------- -}
module Expr where

import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote

data Expr  =  IntExpr Integer
           |  AntiIntExpr String
           |  BinopExpr BinOp Expr Expr
           |  AntiExpr String
    deriving(Show, Typeable, Data)

data BinOp  =  AddOp
            |  SubOp
            |  MulOp
            |  DivOp
    deriving(Show, Typeable, Data)

eval :: Expr -> Integer
eval (IntExpr n)        = n
eval (BinopExpr op x y) = (opToFun op) (eval x) (eval y)
  where
    opToFun AddOp = (+)
    opToFun SubOp = (-)
    opToFun MulOp = (*)
    opToFun DivOp = div

expr = QuasiQuoter { quoteExp = parseExprExp, quotePat =  parseExprPat }

-- Parse an Expr, returning its representation as
-- either a Q Exp or a Q Pat. See the referenced paper
-- for how to use SYB to do this by writing a single
-- parser of type String -> Expr instead of two
-- separate parsers.

parseExprExp :: String -> Q Exp
parseExprExp ...

parseExprPat :: String -> Q Pat
parseExprPat ...

Теперь запустите компилятор:

$ ghc --make -XQuasiQuotes Main.hs -o main

Запустите «main» — и получите следующий вывод:

$ ./main
3
1

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.14.1/docs/users_guide/exts/template_haskell.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API