Использование 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
space :: LaTeX
space = " \\quad "

leq :: LaTeX
leq = " \\leq "

и т.п. С переходом на новую версию HaTeX’а, я переделал их таким образом:

1
2
3
4
5
space :: LaTeX
space = TeXComm "quad" []

leq :: LaTeX
leq = TeXComm "leq" []

Это конечно не бог весть какой код, но когда таких функций 10 или 20, мне он всё же кажется достаточно скучным и я нашёл в этом повод для себя попробовать использовать Template Haskell. То есть я хочу генерировать такие объявления автоматически по имени команды и функции. Так что теперь этот код выглядит у меня следующим образом:

1
2
3
defTeXCommand "space" "quad"

defTeXCommand "" "leq"     -- имя функции и команды одинаковые

Повторюсь, что задача высосана из пальца, но её можно воспринимать как простой пример.

Решение

Собственно бо’льшую часть моего подхода я описал в предыдущем посте. Сначала я конечно сделал всё вручную, настрадался, а потом уже задумался о том, как это можно улучшить. Шаблоны должны быть в отдельном модуле, таковы правила TH:

1
2
3
4
5
6
7
8
9
10
11
12
module HaTeXExtension.Meta where



defTeXCommand :: String   -- ^ function name
                String   -- ^ TeX command name
                Q [Dec]  -- ^ top-level declaration
defTeXCommand  ""  comm = defTeXCommand comm comm
defTeXCommand name comm = sequence
    [ name ^:: [t| LaTeX |]
    , name ^= [| TeXComm comm [] |]
    ]

Вроде бы выглядит достаточно просто и понятно. Объявляем функцию: сигнатура, определение. Тип процитирован, тело процитировано. В теле есть подстановка comm, которая заменится на соответствующую строку.

Дополнения

Дальше я буду усложнять задачу и попытаюсь автоматизировать всё, что можно в HaTeXExtension. Причём то, что я сейчас пишу, я придумал только что “)

Операторы-синонимы

Я говорил о том, что использую юникодовские символы для обозначения операторов соответствующих TeX’овских команд. Вроде такого:

1
2
a  b = a  space  b
a  b = a  leq  b

Ну и коль скоро мы генерим объявления для space и leq, стоит заодно генерить и объявления этих операторов. Чтобы сделать эту возможность гибкой и опциональной, будем передавать в шаблон список строк с именами операторов, так что можно будет оставить его пустым или наоборот написать несколько вариантов имени - например юникодовский и обычный. Причём оператором в данном случае может быть не только инфиксный оператор из спец-символов - если в этом списке будет буквенное имя, то получится просто функция от двух аргументов с таким именем.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
defTeXCommand ::  String
                 String
                [String]   -- ^ operator names
                 Q [Dec]
defTeXCommand  ""  comm ops = defTeXCommand comm comm ops
defTeXCommand name comm ops = sequence $
    [ name ^:: [t| LaTeX |]
    , name ^= body
    ] ++ (concatMap opDec ops)
    where
        body = [| TeXComm comm [] |]
        opDec op = [ op ^:: [t| LaTeX  LaTeX  LaTeX |]
                   , op ^= [| λ a b  a  $body  b |]
                   ]

Разберём изменения в этом шаблоне. Мы обрабатываем список имён операторов функцией opDec, на каждый оператор получается двухэлементный список (сигнатура и определение), поэтому мы применяем concatMap, а не просто map.

В определении оператора есть интересный момент. Я вынес тело определения body отдельно, чтобы использовать его и в декларации name, и в op. По идее стоило бы вставить в конструкцию a ◇ … ◇ b само имя name, но я не знаю, как на него там сослаться и не уверен, что это вообще возможно.

Оператор ^= принимает второй аргумент цитату выражения (:: Q Exp), поэтому body там используется как есть. А вот в определении op мы находимся уже внутри цитаты, поэтому там нужно расцитировать body и вклеить - это и есть сплайсинг. Это работает как-то так:

1
2
3
    [| λ a b  a           $body            b |]
 ~> [| λ a b  a  $([| TeXComm comm [] |])  b |]
 ~> [| λ a b  a      (TeXComm comm [])     b |]

Ну и пример для ясности. Положим, основном модуле HaTeXExtension есть такой текст:

1
2
3
4
5
6
7
import HaTeXExtension.Meta



defTeXCommand "leq_" "leq" ["<=:","≤","leq"]


то есть мы определяем “символ” leq_, соответствующий команде \leq в TeX’е и набор операторов-синонимов. Этот шаблон развернётся при компиляции в следующие декларации:

1
2
3
4
5
6
7
8
9
10
11
leq_ :: LaTeX
leq_ = TeXComm "leq" []

<=: :: LaTeX  LaTeX  LaTeX
<=: = λ a b  ((a  TeXComm "leq" )  b)

 :: LaTeX  LaTeX  LaTeX
 = λ a b  ((a  TeXComm "leq" [])  b)

leq :: LaTeX  LaTeX  LaTeX
leq = λ a b  ((a  TeXComm "leq" [])  b)

Что и требовалось, как говорится. Кстати, если честно, я немного упрощаю вид того, что получается. Дело в том, что при реальном разворачивании шаблона для всех локальных переменных генерируются уникальные идентификаторы, поэтому все эти a и b буду выглядеть как a[a3cm] и b[a3cn].

Кстати, раз уж я об этом заговорил, поскольку разворачивание шаблонов происходит на стадии компиляции, по-умолчанию вы не увидите, во что они развернулись. Для того, чтобы увидеть, нужно загружая модуль в ghci указать ему опцию -ddump-splices и тогда он по ходу загрузки напишет результаты развёртки всех шаблонов - очень удобно.

UPDATE: Я понял, как можно сплайсить имя созданной функции, вместо body:

1
2
3
4
5
6
7
8
defTeXCommand name comm ops = sequence $
    [ name ^:: [t| LaTeX |]
    , name ^= [| TeXComm comm [] |]
    ] ++ (concatMap opDec ops)
    where
        opDec op = [ op ^:: [t| LaTeX  LaTeX  LaTeX |]
                   , op ^= [| λ a b  a  $(dyn name)  b |]
                   ]

Тут тело определения осталось там где и было сначала. Теперь чтобы сплайсить имя, нужно сделать из него процитированное выражение то есть Q Exp, содержащее соответствующий идентификатор. Для этого мы делаем “динамическую связку”(dynamic binding) с помощью функции

1
2
dyn :: String  Q Exp
dyn name = return (VarE (mkName name))

в документации написано, что она “не гигиеничная”. Я подозреваю, что мой подход с подстановкой имён в декларации в принципе не гигиеничный - могут возникнуть конфликты имён. Наверное поэтому это и не сделано по умолчанию. Но я не очень понимаю так ли велика проблема - ведь если буду случайно две функции с одинаковым именем, то ошибка выявится при компиляции..

На сегодня всё. У меня есть ещё дополнения, но что-то я совсем устал, так что напишу о них завтра.

Comments