Заметка о Template Haskell

(репост из tumblr)

Вступление

На развёрнутый пост о своих многочисленных экспериментах с Template Haskell (далее коротко TH) у меня сейчас времени нет, но я его обязательно напишу. В планах даже перевод небольшого туториала.

Эта заметка будет с небольшим вопросом. Я понимаю, что Simon P. Jones (автор TH) борется за чистоту и хочет чтобы везде был строгий контроль типов. На эту тему есть хороший пост в блоге GHC: New directions for Template Haskell. Я не всё там понял, но главное, понял, что всем ограничениям (и недоработкам) есть свои причины.

Постановка задачи

Мне захотелось написать с помощью TH следующую вещь, в сущности, очень простую: хочу генерить функции с данным именем и части тела. Даже ещё проще, пусть функции без параметров - по сути просто значение данного типа (тип заранее известен). Для простоты, пусть у нас есть такой тип:

1
data Foo = Bar String

И мы хотим генерить функции (не функции конечно, а объявления) вида:

1
2
name :: Foo
name = Bar content

где 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
template :: String  String  Q [Dec]
template name content = let name' = newName name
                        in [d| $name' = Bar content |]

(Тип Q [Dec] значит, что в результате получаются “top-level declarations”) Просто написать [d| name = Bar content |] я тоже не могу. (newName чтобы сделать имя функции из строки (специальный тип Name)).

Но ладно бы было просто какое-то объяснение - нельзя значит нельзя. Меня удивляет другое - это можно сделать, если строить синтаксическое дерево вручную! Показываю как:

1
2
3
4
5
template name content =
    valD (varP (mkName name))
         (normalB (appE (conE 'Bar) 
                        (litE (stringL content)))) 
         []

Это выглядит ужасно! Сравните с естественным квазицитированием с стиле лиспа: [d| $name = Bar $content |]. Небо и земля!

При этом я ещё научился пользоваться клёвыми конструкциями из TH.Lib, а сначала делал, используя только чистый синтаксис TH.Syntax. В общем это ужасно. А хочется цитировать конструкции, чтобы синтаксическое дерево строилось для них автоматически.

К счастью то, что стоит справа от знака = в объявлении - это просто выражение (Exp), поэтому его можно процитировать и сократить немного этот код:

1
2
3
4
template name content =
    valD (varP (mkName name))
         (normalB [| Bar cont |])
         []

Это уже лучше. Но всё же меня мучает вопрос, почему подставить имя в такую конструкцию можно, а в цитату нельзя.

Аналогичные проблемы и у объявления сигнатуры (buz :: Foo), но они решаются так же:

1
sigD (mkName name) (conT ''Foo)

или

1
sigD (mkName name) [t| Foo |]

что для более сложного типа было бы намного удобнее.

Реше Уход от проблемы

Итак, мой вопрос озвучен, теперь я напишу, как я решил его для себя. Как видно, для моей задачи совсем обойтись без конструкторов AST не получается. Поэтому я решил просто спрятать их за небольшим синтаксическим сахаром, чтобы при построении других подобных конструкций не вспоминать про все эти ужасные штуки sigD, conT и т.д. и т.п.

Сахар

Для сигнатуры всё просто. Я уже показал, как можно цитировать тип. В качестве улучшения, я хочу забыть про mkName - его всё равно всегда приходится делать. Ну и разумеется долой sigD - вместо него мы я хочу оператор, похожий на обычное ::

1
2
(^::) :: String  Q Type  Q Dec
name ^:: typeQ = sigD (mkName name) typeQ

Пример использования будет чуть дальше. Теперь о декларации самого определения функции. Мы можем цитировать тело, отлично, мы можем снова избавиться от mkName и всяких varP, и снова сделать оператор похожий на то, что он делает:

1
2
(^=) :: String  Q Exp  Q Dec
name ^= bodyQ = valD (varP (mkName name)) (normalB bodyQ) []

Замечательно. Определим ещё одну вспомогательную функцию. Она нужна для того, чтобы объединять одиночные декларации Q Dec в список Q [Dec], поскольку именно в таком виде их нужно потом сплайсить (на top-level’е).

1
2
qDecs :: [Q Dec]  Q [Dec]
qDecs = mapM id

Эту штуку мы получили вообще даром. Для справки тип 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
defFooFunc name content = sequence
    [ name ^:: [t| Foo |]
    , name ^= [| Bar content |]
    ]

Ура!!! '\(^__^)/' Симпатично получилось, правда?

Пользоваться таким шаблоном очень просто. Единственное, что сплайсить шаблоны нельзя в том же модуле, где они определены. На это тоже есть причины… Зато топовые объявления можно сплайсить без этого дурацкого $( … ).

Итак, если я где-нибудь в другом модуле напишу

1
2
3
4
5
6
7
module AnotherModule where



defFooFunc "baz" "quux"


То при конпеляции эта строчка заменится на

1
2
baz :: Foo
baz = Bar "quux"

Собственно, что и требовалось. В моём решении самое главное то, что шаблон выглядит почти также как результат - так и должно быть вообще-то.

Приятным открытием для меня было то, что если перед defFooFunc "baz" "quux" написать комментарий в формате Haddoc (-- | Blah-blah-blah), то он нормально прицепится к сигнатуре и документация сгенерится так же, как при обычном объявлении. Это и естественно, ведь все разворачивания шаблонов происходят при конпеляции.

Заключение

Разумеется моё решение, хоть и выглядит симпатично, не является универсальным. Не любую синтаксическу конструкцию определения функции можно так описать. Но тем не менее, так можно описывать и более сложные функции с несколькими параметрами и клозами. Например вот такое объявление:

1
2
3
4
foo :: Num a => a  String
foo x 1 = show (x + 23)
foo x 2 = show (x - 98)
foo x _ = show x ++ "blah-blah"

где имя функции foo, числа 23, 98 и строчка "blah-blah" - подстановочные. Как это делается:

1
2
3
4
5
6
7
8
9
fooTemplate :: Num a => String  a  a  String  Q [Dec]
fooTemplate name y z blah = sequence
    [ name ^:: [t| Num a => a  String |]
    , name ^= [| λ a b  case (a,b) of
                  (x,1)  show (x + y)
                  (x,2)  show (x - z)
                  (x,_)  show x ++ blah
              |]
    ]

Ну и соответственно $(fooTemplate "foo" 23 98 "blah-blah") сплайсится в то определение, которое мы рассматриваем. Нет, ну конечно не в то же самое. Да, там было три клоза, а у в шаблоне один.. Ну и что, функция ведь получилась такая же. Может кто-нибудь знает, почему не такая же - буду рад узнать.

Меня же такой подход расстраивает только тем, что шаблон опять же выглядит не совсем так, как желаемый результат. Эта неудобная конструкция λ a b → case (a,b) of … конечно не радует глаз, но пока у меня нет прямого решения. Я пробовал сделать это напрямую, но пока у меня это не получилось, потому что возникают проблемы с образцами, а процитировать отдельно клоз нельзя.

UPDATE: Я решил эту проблему и описал решение в другом посте.

P.S. как-то у меня не получается писать совсем коротко. Пост сначала назывался “Короткая заметка …”, но потом я увлёкся и пришлось изменить название. Я хотел написать только коротко о проблеме и моём решении, но без краткого введения в Template Haskell и сопутствующих тем не обошлось. Наверное потому, что мне представляется читатель, который не знает толком этих всех штук и которого надо хотя бы в общем ввести в курс дела. По видимому, это психологическая проекция, ведь обычно, я сам оказываюсь таким несведущим читателем, интересных, но сложных статей…

Comments