Spec-Zone.ru › Haskell 9

6.13. Шаблонный Haskell

Шаблонный Haskell позволяет выполнять метапрограммирование на этапе компиляции в Haskell. Основы основных технических инноваций обсуждаются в статье «Шаблонная метапрограммирование для Haskell» (Труды мастерской Haskell 2002).

На странице Шаблонный Haskell на вики-сайте GHC содержится множество информации. Вы также можете обратиться к справочной документации Haddock по Language.Haskell.TH. Многие изменения по отношению к исходному дизайну описаны в Заметки по версии 2 Шаблонного Haskell. Однако не все эти изменения внедрены в GHC.

Первый пример из этой статьи представлен ниже (Пример работы с Шаблонным Haskell) в качестве примера, чтобы помочь вам начать.

Документация здесь описывает реализацию Шаблонного Haskell в GHC. Ее недостаточно для понимания Шаблонного 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 (т.е. без вставок на верхнем уровне). Расширение TemplateHaskellQuotes считается безопасным в Безопасном Haskell, в то время как TemplateHaskell нет.

  • Вставка записывается $x, где x — произвольное выражение. Между символом “$” и выражением не должно быть пробела. Это использование $ переопределяет его значение как инфиксного оператора, так же как M.x переопределяет значение . как инфиксного оператора. Если вам нужен инфиксный оператор, поставьте пробелы вокруг него.

    Вставка на верхнем уровне может быть расположена вместо

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

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

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

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

    • [| ... |], или [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, но содержит много других операций, которые не нужны для определения цитат.

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

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

    oneC, twoC, plusC  :: Quote m => m Exp
    oneC = [| 1 |]
    
    twoC = [| 2 |]
    
    plusC = [| $oneC + $twoC |]
    
  • Точный тип цитаты зависит от типов вложенных вставок внутри нее:

    -- 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
    
  • Типизированная вставка выражения записывается $$x, где x — произвольное выражение.

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

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

  • Типизированная цитата выражения записывается как [|| ... ||], или [e|| ... ||], где “…” — это выражение; если выражение “…” имеет тип a, то цитата имеет тип Quote m => Code m a.

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

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

    • [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.

  • Возможна ситуация, когда вставка расширяется до выражения, содержащего имена, которые не находятся в области видимости в месте вставки. Рассмотрим следующий код:

    module Bar where
    
    import Language.Haskell.TH
    
    add1 :: Quote m => Int -> m Exp
    add1 x = [| x + 1 |]
    

    Теперь рассмотрим вставку, использующую add1 в отдельном модуле:

    module Foo where
    
    import Bar
    
    two :: Int
    two = $(add1 1)
    

    Template Haskell не может знать, каким будет аргумент к add1 в месте определения функции, поэтому используется механизм подъема для продвижения x до значения типа Quote m => m Exp . Эта функциональность предоставляется пользователю как класс типов 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
    

    В общем случае, если GHC видит выражение в квадратных скобках (например, [| foo bar |], то GHC ищет каждое имя в скобках. Если имя является глобальным (например, предположим, что foo получено из импорта или объявления верхнего уровня), то используется полное имя. Если имя является локальным (например, предположим, что bar связано локально в определении функции mkFoo bar = [| foo bar |] ), то GHC использует lift (так GHC имитирует, что [| foo bar |] на самом деле содержит [| foo $(lift bar) |]). Локальные имена, которые не находятся в области видимости в местах вставок, фактически вычисляются при обработке цитаты.

    Библиотека template-haskell предоставляет экземпляры Lift для многих распространенных типов данных. Кроме того, можно автоматически вывести экземпляры Lift с помощью расширения языка DeriveLift. См. Вывод экземпляров Lift для получения дополнительной информации.

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

    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
    
  • Топ-уровневые вставки объявлений разбивают исходный файл на группы объявлений. Группа объявлений — это группа объявлений, созданная топ-уровневой вставкой объявления, плюс те, которые следуют за ней, вплоть до, но не включая, следующую топ-уровневую вставку объявления. Примечание. только топ-уровневые вставки разделяют группы объявлений, а не вставки выражений. Первая группа объявлений в модуле включает все определения верхнего уровня до, но не включая, первую топ-уровневую вставку объявления.

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

    • Позже группы могут «видеть» объявления и объявления экземпляров из предыдущих групп;
    • Но предыдущие группы не могут «видеть» объявления или объявления экземпляров из последующих групп.

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

    Соответственно, среда типов, видимая 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".
  • Квазицитаторы выражений принимают большинство конструкций языка Haskell. Однако есть некоторые расширения, специфичные для GHC, которые в настоящее время не поддерживаются квазицитаторами выражений, включая

    • Свободные переменные типа во вставках с типом (см. #10945 и #10946)

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

-fenable-th-splice-warnings

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

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

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

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

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

Template Haskell работает в любом режиме (--make, --interactive или пофайлово). Раньше существовало ограничение для первых двух, но это ограничение было снято.

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

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

Флаг -dth-dec-file выводит расширения всех сплейсов объявления верхнего уровня TH, как типизированных, так и нетипизированных, в файл M.th.hs для каждого модуля M который компилируется. Обратите внимание, что другие типы сплейсов (выражения, типы и шаблоны) не отображаются. Разработчики приложений могут включить это в свой репозиторий, чтобы они могли использовать grep для поиска идентификаторов, которые были определены в 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.4. Пример работы с 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.5. Цитирование Template Haskell и синтаксис Rebindable

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

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

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

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

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

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

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

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

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

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

QuasiQuotes
Since:

6.10.1

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

Квазицитирование позволяет записывать шаблоны и выражения с помощью определённого программистом конкретного синтаксиса; мотивация расширения и несколько примеров документированы в статье «Почему приятно быть цитируемым: квазицитирование для Haskell» (Proc Haskell Workshop 2007). Пример ниже демонстрирует, как написать квазицитировщик для простого языка выражений.

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

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

    • ⟨quoter⟩ должен быть именем импортированного квазицитировщика, квалифицированного или нет; это не может быть произвольное выражение.
    • ⟨quoter⟩ не может быть «e», «t», «d» или «p», так как они перекрываются с цитатами Template Haskell.
    • В токене [quoter| не должно быть пробелов.
    • Цитируемая ⟨строка⟩ может быть произвольной и содержать новые строки.
    • Цитируемая ⟨строка⟩ заканчивается на первой встреченной последовательности из двух символов "|]". Абсолютно никакого экранирования не выполняется. Если вы хотите встроить эту последовательность символов в строку, вы должны придумать свою собственную систему экранирования (например, использовать строку "|~]" вместо), и заставить вашу функцию-квазицитировщик интерпретировать "|~]" как "|]". Один из способов реализации — это композиция вашего квазицитировщика с предобработанным проходом, который выполнит преобразование экранирования. Подробнее см. обсуждение в #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.12.1/docs/users_guide/exts/template_haskell.html

Spec-Zone.ru

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