Template Haskell позволяет выполнять метапрограммирование на Haskell во время компиляции. Предпосылки основных технических новшеств рассматриваются в статье «Метапрограммирование с шаблонами для Haskell» (Proc Haskell Workshop 2002). Первый пример из этой статьи приведён ниже (Практический пример использования Template Haskell), чтобы помочь вам начать работу.
6.13.1. Синтаксис
-
TemplateHaskell -
- Включает:
- Начиная с версии:
-
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) позволяет использовать идентификаторы на уровнях, отличных от того, на котором они были определены. Для этого предусмотрено два механизма:
-
Сохранение на основе пути: оно позволяет использовать глобальное определение на другом уровне в двух случаях:
- Любой глобальный идентификатор можно использовать на более позднем уровне (то есть внутри цитаты).
- Импортированный идентификатор можно использовать на более раннем уровне (то есть в сплайсе).
Расширение
ImplicitStagePersistenceуправляет включением сохранения на основе пути. По умолчанию оно включено во всех текущих редакциях языка. -
Сохранение на основе сериализации: оно позволяет использовать локально связанные переменные на более высоких уровнях с помощью класса типов
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 внутри…
- Сплайс
$(th1 ...)увидит определениеf— сплайс находится на верхнем уровне, поэтому видны все определения из предыдущей группы объявлений (то есть все определения модуля вплоть до самого сплайса, не включая его). - Сплайс
$(blah1)не может обращаться к функцииw—wнаходится в последующей группе объявлений и потому невидим. Аналогично,$(blah1)не увидит определениеh, поскольку оно находится в той же группе объявлений, что и$(blah1). Однако сплайс$(blah1)увидит определениеf, поскольку оно находится в непосредственно предшествующей группе объявлений. - Сплайс
$(th2 ...)увидит определениеf, все связывания, созданные$(th1 ...), определениеhи все связывания, созданные[qq|blah|](все они находятся в предыдущих группах объявлений). - Тело
hможет обращаться к функцииk, расположенной по другую сторону квазицитирователя объявлений, поскольку квазицитирователи не разбивают группу объявлений. - Квазицитирователь
qqсможет видеть определениеfиз предшествующей группы объявлений, но не определенияhилиkи не какие-либо определения из последующих групп объявлений. - Сплайс
$(blah2)увидит те же определения, что и сплайс$(th2 ...)(но не связывания, которые он создаёт).
Обратите внимание: поскольку сплайс выражения не может обращаться к объявлениям из той же группы объявлений, для разделения группы можно добавить пустой сплайс верхнего уровня:
module M where data D = C1 | C2 f1 = $(th1 ...) $(return []) f2 = $(th2 ...)
Здесь
- Сплайс
$(th1 ...)не может обращаться кD— оно находится в той же группе объявлений. - Группа объявлений, содержащая
D, завершается пустым сплайсом объявлений верхнего уровня$(return [])(помните,Q— это монада, поэтому достаточно просто выполнитьreturnдля пустого списка объявлений). - Поскольку группа объявлений, содержащая
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 использовать:
- При поиске экземпляра
C1 IntGHC обнаруживает в одной группе объявлений два возможных экземпляра: несогласованный экземплярC1 aи согласованный экземплярC1 Int. Согласно правилам поиска экземпляров, описанным в разделе Перекрывающиеся экземпляры, поскольку для выбора имеется ровно один согласованный экземпляр, GHC выберет экземплярC1 Int. В результатеm1 iбудет эквивалентенshow i(то есть"42"). - При поиске экземпляра
C2 IntGHC обнаруживает в той же группе объявлений только один экземпляр: несогласованный экземпляр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, в том числе:
(По сравнению с исходной статьёй имеется много небольших различий. В синтаксисе сплайса объявления используется «$», а не «splice». Тип заключённого выражения должен быть Quote m => m [Dec], а не [Q Dec]. Типизированные сплайсы и цитаты выражений поддерживаются.)
-
-fenable-th-splice-warnings -
Для сплайсов Template Haskell предупреждения не проверяются, поскольку код, вызывающий предупреждение, может поступать из сторонней библиотеки и, возможно, не был написан пользователем. Чтобы всё же включить предупреждения для сплайсов, передайте
-fenable-th-splice-warnings.