Заметка о Template Haskell
(репост из tumblr)
Вступление
На развёрнутый пост о своих многочисленных экспериментах с Template Haskell (далее коротко TH) у меня сейчас времени нет, но я его обязательно напишу. В планах даже перевод небольшого туториала.
Эта заметка будет с небольшим вопросом. Я понимаю, что Simon P. Jones (автор TH) борется за чистоту и хочет чтобы везде был строгий контроль типов. На эту тему есть хороший пост в блоге GHC: New directions for Template Haskell. Я не всё там понял, но главное, понял, что всем ограничениям (и недоработкам) есть свои причины.
Постановка задачи
Мне захотелось написать с помощью TH следующую вещь, в сущности, очень простую: хочу генерить функции с данным именем и части тела. Даже ещё проще, пусть функции без параметров - по сути просто значение данного типа (тип заранее известен). Для простоты, пусть у нас есть такой тип:
1
|
|
И мы хотим генерить функции (не функции конечно, а объявления) вида:
1 2 |
|
где name
и content
- это заранее неизвестные значения. То есть параметры генератора/шаблона.
Ок, в чём моя личная проблема: я хочу как можно меньше строить AST (абстрактное синтаксическое дерево) вручную, с помощью алгебраических типов из TH.Syntax и даже больше, хочу совсем не использовать. А вместо этого я хочу использовать везде, где можно цитирование (Quoting).
Ну вот насмотрелся я на лисповские макросы с их клёвым квазицитированием и не пойму, почему в TH этого нет “/ Да, я знаю, что там есть квазицитирование, но совсем не такое. Да, возможно я не умею его готовить, но буду рад, если кто-то меня научит.
Немного о TH
Итак, без всяких квазей, в TH есть оксфордские скобки: [| som quoted haskell code here |]
и они бывают 4х типов:
[e| … |]
или[| … |]
для выражений (:: Q Exp
)[d| … |]
для объявлений (:: Q [Dec]
)[t| … |]
для типов (:: Q Type
)[p| … |]
для образцов (паттернов) (:: Q Pat
)
И есть сплайсинг (splicing - не знаю, как нормально перевести) - это операция обратная цитированию: $( … )
. Эта конструкция из цитаты получает сам код.
Как-то так например: $( [| \x → x + $( [| 1 |] ) |] )
— то же самое, что просто \x → x + 1
Проблема
То есть вроде как вот тебе, пожалуйста, цитирование+расцитирование = квазицитирование. Ан нет. Проблема в том, что сплайсить можно не везде. В частности на данный момент нельзя сплайсить в образцах и в типах. Это-то мне и непонятно почему. В посте, на который я дал ссылку вначале есть объяснение (всякие там конфликты имён) и есть ссылки на обсуждения того, как это лучше сделать и т.п. То есть вопрос открытый и довольно давно.
А ведь мне для генерации моих объявлений как раз и нужно сплайсить в образец. То есть я не могу просто сделать так:
1 2 3 |
|
(Тип Q [Dec]
значит, что в результате получаются “top-level declarations”)
Просто написать [d| name = Bar content |] я тоже не могу. (newName чтобы сделать имя функции из строки (специальный тип Name
)).
Но ладно бы было просто какое-то объяснение - нельзя значит нельзя. Меня удивляет другое - это можно сделать, если строить синтаксическое дерево вручную! Показываю как:
1 2 3 4 5 |
|
Это выглядит ужасно! Сравните с естественным квазицитированием с стиле лиспа: [d| $name = Bar $content |]
. Небо и земля!
При этом я ещё научился пользоваться клёвыми конструкциями из TH.Lib, а сначала делал, используя только чистый синтаксис TH.Syntax. В общем это ужасно. А хочется цитировать конструкции, чтобы синтаксическое дерево строилось для них автоматически.
К счастью то, что стоит справа от знака =
в объявлении - это просто выражение (Exp
), поэтому его можно процитировать и сократить немного этот код:
1 2 3 4 |
|
Это уже лучше. Но всё же меня мучает вопрос, почему подставить имя в такую конструкцию можно, а в цитату нельзя.
Аналогичные проблемы и у объявления сигнатуры (buz :: Foo
), но они решаются так же:
1
|
|
или
1
|
|
что для более сложного типа было бы намного удобнее.
Реше Уход от проблемы
Итак, мой вопрос озвучен, теперь я напишу, как я решил его для себя. Как видно, для моей задачи совсем обойтись без конструкторов AST не получается. Поэтому я решил просто спрятать их за небольшим синтаксическим сахаром, чтобы при построении других подобных конструкций не вспоминать про все эти ужасные штуки sigD
, conT
и т.д. и т.п.
Сахар
Для сигнатуры всё просто. Я уже показал, как можно цитировать тип. В качестве улучшения, я хочу забыть про mkName
- его всё равно всегда приходится делать. Ну и разумеется долой sigD
- вместо него мы я хочу оператор, похожий на обычное ::
1 2 |
|
Пример использования будет чуть дальше. Теперь о декларации самого определения функции. Мы можем цитировать тело, отлично, мы можем снова избавиться от mkName
и всяких varP
, и снова сделать оператор похожий на то, что он делает:
1 2 |
|
Замечательно. Определим ещё одну вспомогательную функцию. Она нужна для того, чтобы объединять одиночные декларации Q Dec
в список Q [Dec]
, поскольку именно в таком виде их нужно потом сплайсить (на top-level’е).
1 2 |
|
Эту штуку мы получили вообще даром. Для справки тип mapM :: Monad m => (a → m b) → [a] → m [b]
, а Q
- это кстати монада цитирования (от “Quote”).
UPDATE: что-то я забыл (спасибо eminglorion за напоминание), что есть стандартная функция sequence :: Monad m => [m a] → m [a]
, которая как раз делает то же самое. Так что дальше я везде исправлю qDecs
на sequence
.
Итак, генератор, а правильнее сказать шаблон, который я хотел получить с самого начала будет выглядеть так:
1 2 3 4 |
|
Ура!!! '\(^__^)/'
Симпатично получилось, правда?
Пользоваться таким шаблоном очень просто. Единственное, что сплайсить шаблоны нельзя в том же модуле, где они определены. На это тоже есть причины… Зато топовые объявления можно сплайсить без этого дурацкого $( … ).
Итак, если я где-нибудь в другом модуле напишу
1 2 3 4 5 6 7 |
|
То при конпеляции эта строчка заменится на
1 2 |
|
Собственно, что и требовалось. В моём решении самое главное то, что шаблон выглядит почти также как результат - так и должно быть вообще-то.
Приятным открытием для меня было то, что если перед defFooFunc "baz" "quux"
написать комментарий в формате Haddoc (-- | Blah-blah-blah
), то он нормально прицепится к сигнатуре и документация сгенерится так же, как при обычном объявлении. Это и естественно, ведь все разворачивания шаблонов происходят при конпеляции.
Заключение
Разумеется моё решение, хоть и выглядит симпатично, не является универсальным. Не любую синтаксическу конструкцию определения функции можно так описать. Но тем не менее, так можно описывать и более сложные функции с несколькими параметрами и клозами. Например вот такое объявление:
1 2 3 4 |
|
где имя функции foo
, числа 23
, 98
и строчка "blah-blah"
- подстановочные. Как это делается:
1 2 3 4 5 6 7 8 9 |
|
Ну и соответственно $(fooTemplate "foo" 23 98 "blah-blah")
сплайсится в то определение, которое мы рассматриваем. Нет, ну конечно не в то же самое. Да, там было три клоза, а у в шаблоне один.. Ну и что, функция ведь получилась такая же. Может кто-нибудь знает, почему не такая же - буду рад узнать.
Меня же такой подход расстраивает только тем, что шаблон опять же выглядит не совсем так, как желаемый результат. Эта неудобная конструкция λ a b → case (a,b) of …
конечно не радует глаз, но пока у меня нет прямого решения. Я пробовал сделать это напрямую, но пока у меня это не получилось, потому что возникают проблемы с образцами, а процитировать отдельно клоз нельзя.
UPDATE: Я решил эту проблему и описал решение в другом посте.
P.S. как-то у меня не получается писать совсем коротко. Пост сначала назывался “Короткая заметка …”, но потом я увлёкся и пришлось изменить название. Я хотел написать только коротко о проблеме и моём решении, но без краткого введения в Template Haskell и сопутствующих тем не обошлось. Наверное потому, что мне представляется читатель, который не знает толком этих всех штук и которого надо хотя бы в общем ввести в курс дела. По видимому, это психологическая проекция, ведь обычно, я сам оказываюсь таким несведущим читателем, интересных, но сложных статей…