Pull to refresh

Введение в Template Haskell. Часть 3. Прочие аспекты TH

Reading time6 min
Views2.5K
Original author: Булат Зиганшин
Данный текст является переводом документации Template Haskell, написанной Булатом Зиганшиным. Перевод всего текста разбит на несколько логических частей для облегчения восприятия. Далее курсив в тексте — примечания переводчика. Предыдущие части:


Материализация


Материализация (reification) — это средство Template Haskell, позволяющее программисту получить информацию из таблицы символов компилятора. Монадическая функция reify ∷ Name → Q Info возвращает информацию о данном имени: если это глобальный идентификатор (функция, константа, конструктор) – вы получите его тип, если это тип или класс – вы получите его структуру. Определение типа Info можно найти в модуле Language.Haskell.TH.Syntax.
Материализация может быть использована для того, чтобы получить структуру типа, но таким образом нельзя получить тело функции. Если вам нужно материализовать тело функции, то определение функции нужно процитировать и дальше можно будет работать с этим определением с помощью другого шаблона. Например так:
$(optimize [d| fib = … |])

или так
fib = $(optimize [| … |])

На самом деле, в оригинальной статье больше ничего не говорится про материализацию. Не знаю, насколько это содержательная тема – необходимый минимум знаний о ней ограничивается функцией reify и типом Info, но есть некоторые тонкости, связанные например с тем, что можно получить информацию не о любом имени. Если эта тема интересна, я могу собрать какую-нибудь информацию и написать об этом отдельную заметку (или вклеить сюда).

Облегчённое цитирование имён


Чтобы получить имя (∷ Name), соответствующее интересующему идентификатору, можно использовать функцию mkName, но это не безопасное решение, потому что mkName возвращает не квалифицированное имя, которое может интерпретироваться по-разному в зависимости от контекста. А вот код VarE id ← [| foo |] безопасен в этом смысле, так как цитирование квалифицирует имена (получится что-то типа My.Own.Module.foo), но этот код слишком многословный и требует монадический контекст для использования. К счастью, Template Haskell, имеет другую простую форму цитирования имён: 'foo (одинарная кавычка перед foo) имеет тип Name и содержит квалифицированное имя, соответствующее идентификатору foo, так что код let id = 'foo эквивалентен по смыслу коду VarE id ← [| foo |]. Обратите внимание, что эта конструкция имеет простой тип Name (а не Q Exp или Q Name), так что она может быть использована там, где не возможно использование монад, например:
f ∷ Exp → Exp
f (App (Var m) e) |  m == 'map  =  …

Эта новая форма тем не менее является цитированием и подчиняется тем же правилам, что и цитирующие скобки [| … |]. Например, она не может быть использована внутри этих скобок (так нельзя: [| 'foo |]), но и вклеивание к ней не может быть применено (так тоже нельзя: $( 'foo )), потому что для вклейки нужен тип Q …. Более важно то, что эта форма определяется статически, возвращая полностью квалифицированное имя, с однозначной интерпретацией.
Haskell’евские пространства имён немного всё усложняют. Цитата [| P |] означает конструктор данных P, в то время как [t| P |] означает конструктор типа P. Поэтому для “облегчённого цитирования” необходим такой же способ разделения этих сущностей. Для контекста типов используется просто две одинарные кавычки:
  • 'Foo означает “конструктор данных Foo в контексте выражения”
  • 'foo означает “имя foo в контексте выражения”
  • ''Foo означает “конструктор типа Foo в контексте типов”
  • ''foo означает “переменная типа foo в контексте типов”
Облегчённая форма цитирования используется в примере генерации воплощений класса Show, который разбирается в конце.


Сообщения об ошибках и восстановление


Монада цитирования позволяет передавать сообщения об ошибках и восстанавливаться.
report ∷ Bool → String → Q ()

Функция report выводит данное во втором аргументе сообщение. Если первый аргумент True, то результат воспринимается как ошибка, иначе, сообщение просто показывается (как warning). В любом случае, вычисления продолжаются, если их нужно остановить, используйте монадный fail. Если “закрывающего” recover нет, компиляция завершается неудачно.
recover ∷ Q a → Q a → Q a

Вызов recover a b запускает b. Если в b были вызовы report True "…", то запускается a. Если b завершается без таких ошибок, то возвращается его результат, а a игнорируется.
location ∷ Q Loc

Возвращает координаты места в коде (в виде структуры Loc), где происходит текущее вклеивание – может быть удобно для сообщений об ошибках.

Отладка


Для того, чтобы упростить отладку TH-программ, GHC поддерживает специальный флаг -ddump-splices, с которым он показывает результаты вклейки всех шаблонов верхнего уровня во время загрузки модуля.
Кроме этого, в интерпретаторе можно запускать вычисления в монаде Q с помощью функции runQ ∷ Q a → IO a и выводить результат либо в виде абстрактного синтаксиса (AST), либо в виде соответствующего Haskell-кода:
$ ghci –XTemplateHaskell
…

Prelude> :m + Language.Haskell.TH 

Prelude Language.Haskell.TH> runQ [| \x _ -> x |] >>= print
LamE [VarP x_1,WildP] (VarE x_1)

Prelude Language.Haskell.TH> runQ [| \x _ -> x |] >>= putStrLn . pprint 
\x_0 _ -> x_0

Функция pprint выводит Haskell-код в том виде, как он будет вклеен в программу при компиляции. Для дальнейших примеров напишем простой шаблон, который генерирует лямбда-выражение, игнорируещее свои аргументы и возвращающее данную строку:
module Cnst where

import Language.Haskell.TH

cnst :: Int -> String -> Q Exp
cnst n s = return (LamE (replicate n WildP) (LitE (StringL s)))

Теперь его можно потестировать в интерпретаторе:
$ ghci -XTemplateHaskell Cnst.hs  
…

*Cnst> runQ (cnst 2 "str") >>= print
LamE [WildP,WildP] (LitE (StringL "str"))

*Cnst> runQ (cnst 2 "str") >>= putStrLn . pprint 
\_ _ -> "str"

То же самое можно делать и в модулях, которые импортируют шаблоны:
{-# LANGUAGE TemplateHaskell #-}

module Main where

import Language.Haskell.TH
import Cnst

-- шаблон cnst можно вклеивать как выражение:
cnst1 ::  t -> [Char]
cnst1 = $(cnst 1 "x")

cnst2 ::  t1 -> t2 -> [Char]
cnst2 = $(cnst 2 "str")

-- аналогично, будет тип с 20ю аргументами
cnst20 = $(cnst 20 "foo")

-- так же как в интерпретаторе, можно использовать runQ:
main = do runQ(cnst 1 "x")    >>= print
          runQ(cnst 2 "str")  >>= print
          runQ(cnst 20 "foo") >>= print
          runQ(cnst 1 "x")    >>= putStrLn.pprint
          runQ(cnst 2 "str")  >>= putStrLn.pprint
          runQ(cnst 20 "foo") >>= putStrLn.pprint


Пример: deriveShow



Это небольшой пример, демонстрирующий, как Template Haskell может использоваться для автоматической генерации воплощений (instances) классов. Он использует конструкторы AST, облегчённое цитирование, цитирующие скобки, материализацию – в общем, практически всё, что обсуждалось в этой статье. Для простоты, шаблон работает только для простых алгебраических типов (без параметризации, без именованных полей и т.п.) Я внёс пару изменений (в целях улучшения читабельности) в оригинальный код, в том числе вынес функцию showClause отдельно.
  • Main.hs

    {-# LANGUAGE TemplateHaskell #-}

    module Main where

    import Derive

    data T = A Int String | B Integer | C
    $(deriveShow ''T)

    main = print [A 1 "s", B 2, C]  -- печатает в точности [A 1 "s",B 2,C]

  • Derive.hs

    {-# LANGUAGE TemplateHaskell #-}

    module Derive where

    import Language.Haskell.TH
    import Control.Monad

    data T1 = T1

    -- Вспомогательная функция, которая генерирует 
    -- n уникальных имён для образцов и выражений
    genPE :: Int -> Q ([PatQ], [ExpQ])
    genPE n = do
        ids <- replicateM n (newName "x")
        return (map varP ids, map varE ids)

    -- Собираем один клоз функции show для данного конструктора:
    --   show (A x1 x2) = "A "++show x1++" "++show x2
    showClause :: Con -> Q Clause
    showClause (NormalC name fields) = do
        -- Имя конструктора, т.е. "A". nameBase возвращает имя без квалификации
        let constructorName = nameBase name
        -- Генерируем имена переменных для левой и правой части определения 
        (pats,vars) <- genPE (length fields)
        -- Рекурсивно строим выражение (" "++show x1++...++"") из списка переменных [x1, ...]
        let f []       = [| constructorName |]
            f (v:vars) = [| " " ++ show $v ++ $(f vars) |]
        -- Собираем один клоз функции
        clause [conP name pats]       -- (A x1 x2)
               (normalB (f vars)) []  -- "A"++" "++show x1++" "++show x2

    -- Основной шаблон, который генерирует объявление воплощения класса Show
    deriveShow :: Name -> Q [Dec]
    deriveShow t = do
        -- Получаем список конструкторов данных для типа t
        TyConI (DataD _ _ _ constructors _) <- reify t

        -- Теперь собираем все клозы в тело функции show:
        --   show (A x1 x2) = "A "++show x1++" "++show x2
        --   show (B x1)    = "B "++show x1
        --   show C         = "C"
        showbody <- mapM showClause constructors

        -- Генерируем шаблон объявления воплощения и потом заменяем в нём
        -- имя типа (T1) и тело функции (x = "text") нашим сгенерированным showbody
        d <- [d| instance Show T1 where
                    show x = "text"
              |]
        let    [InstanceD [] (AppT showt (ConT _T1)) [FunD showf _text]] = d
        return [InstanceD [] (AppT showt (ConT t  )) [FunD showf showbody]]


Заключение


На этом статья заканчивается, в ней были освещены основные темы в объёме, достаточном для того, чтобы начать использовать метапрограммирование на Haskell, так что можно считать, что введение в Template Haskell состоялось. Для дальнейшего углубления в тему предлагается заглянуть на официальную страничку TH, на которой можно найти ссылки на другие статьи и множество примеров.
Tags:
Hubs:
Total votes 15: ↑13 and ↓2+11
Comments1

Articles