Использование Template Haskell в HaTeXExtension
(репост из tumblr)
Вступление
За последние дни я много экспериментировал с Template Haskell и узнал много интересных вещей. Я по прежнему многое ещё не понимаю, но со многими вопросами я разобрался и хочу об этом тут написать. Сразу оговорюсь, что мои ответы скорее всего являются следствием того, что я не умею пока готовить TH, или я просто изобретаю самокаты.
Итак, на этой неделе я уже написал два поста о HaTeX-3 и Template Haskell. В последнем посте я ставил вопрос о генерации объявлений с помощью TH и непонятной мне невозможности замены имени объявляемого объекта в цитированном коде. (Ну и оборот завернул, это у меня после написания эссе по философии)
Сегодня я немного поразбирался с квазицитированием (Quasiquotation, далее коротко QQ) - ещё одним расширением Haskell’а, на базе TH. Рекомендую почитать статью по ссылке и разобрать несложный пример - на нём многое становится понятно. Поначалу я недоумевал - ну парсеры, ну с другим синтаксисом, ну и что… Но сегодня я наконец-то осознал в чём истинная сила QQ и обязательно напишу об этом на следующей неделе. У меня уже есть пара идей для реализации с помощью QQ и в том числе для HaTeX.
Постановка задачи
Задача на самом деле остаётся та же, что и в предыдущем посте про TH. Только теперь я переформулирую её в контексте моих экспериментов с HaTeX. Поскольку каких-то TeX’овских команд мне не хватало в HaTeX’е, я решил написать для него небольшое дополнение. Итак, в моём модуле HaTeXExtension была куча объявлений вида
1 2 3 4 5 |
|
и т.п. С переходом на новую версию HaTeX’а, я переделал их таким образом:
1 2 3 4 5 |
|
Это конечно не бог весть какой код, но когда таких функций 10 или 20, мне он всё же кажется достаточно скучным и я нашёл в этом повод для себя попробовать использовать Template Haskell. То есть я хочу генерировать такие объявления автоматически по имени команды и функции. Так что теперь этот код выглядит у меня следующим образом:
1 2 3 |
|
Повторюсь, что задача высосана из пальца, но её можно воспринимать как простой пример.
Решение
Собственно бо’льшую часть моего подхода я описал в предыдущем посте. Сначала я конечно сделал всё вручную, настрадался, а потом уже задумался о том, как это можно улучшить. Шаблоны должны быть в отдельном модуле, таковы правила TH:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Вроде бы выглядит достаточно просто и понятно. Объявляем функцию: сигнатура, определение. Тип процитирован, тело процитировано. В теле есть подстановка comm
, которая заменится на соответствующую строку.
Дополнения
Дальше я буду усложнять задачу и попытаюсь автоматизировать всё, что можно в HaTeXExtension. Причём то, что я сейчас пишу, я придумал только что “)
Операторы-синонимы
Я говорил о том, что использую юникодовские символы для обозначения операторов соответствующих TeX’овских команд. Вроде такого:
1 2 |
|
Ну и коль скоро мы генерим объявления для space
и leq
, стоит заодно генерить и объявления этих операторов. Чтобы сделать эту возможность гибкой и опциональной, будем передавать в шаблон список строк с именами операторов, так что можно будет оставить его пустым или наоборот написать несколько вариантов имени - например юникодовский и обычный. Причём оператором в данном случае может быть не только инфиксный оператор из спец-символов - если в этом списке будет буквенное имя, то получится просто функция от двух аргументов с таким именем.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Разберём изменения в этом шаблоне. Мы обрабатываем список имён операторов функцией opDec
, на каждый оператор получается двухэлементный список (сигнатура и определение), поэтому мы применяем concatMap
, а не просто map
.
В определении оператора есть интересный момент. Я вынес тело определения body
отдельно, чтобы использовать его и в декларации name
, и в op
. По идее стоило бы вставить в конструкцию a ◇ … ◇ b
само имя name
, но я не знаю, как на него там сослаться и не уверен, что это вообще возможно.
Оператор ^=
принимает второй аргумент цитату выражения (:: Q Exp
), поэтому body
там используется как есть. А вот в определении op
мы находимся уже внутри цитаты, поэтому там нужно расцитировать body
и вклеить - это и есть сплайсинг. Это работает как-то так:
1 2 3 |
|
Ну и пример для ясности. Положим, основном модуле HaTeXExtension есть такой текст:
1 2 3 4 5 6 7 |
|
то есть мы определяем “символ” leq_
, соответствующий команде \leq
в TeX’е и набор операторов-синонимов. Этот шаблон развернётся при компиляции в следующие декларации:
1 2 3 4 5 6 7 8 9 10 11 |
|
Что и требовалось, как говорится. Кстати, если честно, я немного упрощаю вид того, что получается. Дело в том, что при реальном разворачивании шаблона для всех локальных переменных генерируются уникальные идентификаторы, поэтому все эти a
и b
буду выглядеть как a[a3cm]
и b[a3cn]
.
Кстати, раз уж я об этом заговорил, поскольку разворачивание шаблонов происходит на стадии компиляции, по-умолчанию вы не увидите, во что они развернулись. Для того, чтобы увидеть, нужно загружая модуль в ghci указать ему опцию -ddump-splices
и тогда он по ходу загрузки напишет результаты развёртки всех шаблонов - очень удобно.
UPDATE:
Я понял, как можно сплайсить имя созданной функции, вместо body
:
1 2 3 4 5 6 7 8 |
|
Тут тело определения осталось там где и было сначала. Теперь чтобы сплайсить имя, нужно сделать из него процитированное выражение то есть Q Exp
, содержащее соответствующий идентификатор. Для этого мы делаем “динамическую связку”(dynamic binding) с помощью функции
1 2 |
|
в документации написано, что она “не гигиеничная”. Я подозреваю, что мой подход с подстановкой имён в декларации в принципе не гигиеничный - могут возникнуть конфликты имён. Наверное поэтому это и не сделано по умолчанию. Но я не очень понимаю так ли велика проблема - ведь если буду случайно две функции с одинаковым именем, то ошибка выявится при компиляции..
На сегодня всё. У меня есть ещё дополнения, но что-то я совсем устал, так что напишу о них завтра.