Использование 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 | |
в документации написано, что она “не гигиеничная”. Я подозреваю, что мой подход с подстановкой имён в декларации в принципе не гигиеничный - могут возникнуть конфликты имён. Наверное поэтому это и не сделано по умолчанию. Но я не очень понимаю так ли велика проблема - ведь если буду случайно две функции с одинаковым именем, то ошибка выявится при компиляции..
На сегодня всё. У меня есть ещё дополнения, но что-то я совсем устал, так что напишу о них завтра.