1. Что такое Hy
Hy — диалект Лиспа, который встроен в питон.
Благодаря тому, что Hy трансформирует свой Лиспоподобный код в Абстрактное Синтаксическое Дерево (AST) питона, с помощью Hy весь прекрасный мир питона — на кончиках пальцев и в форме Лиспа.

2. О синтаксисе Hy, очень кратко
Hy — своеобразный язык, похожий на каждого из своих родителей (больше, конечно, на Лисп). Для тех, кто не знаком с синтаксисом Лиспа, его можно в данном случае суммировать так.
- Отступ не играет роли. Вместо этого — уровни вложенности в выражения из круглых скобочек.
- Во всех вызовах функций название функции попадает в скобки со списком аргументов на первое место; запятые в списке аргументов не используются.
- Все операторы записываются так, как будто они — функции.
- Двоеточия не используются.
- Литералы для строк и словарей работают как и раньше; строки записываются в двойных кавычках, кортежи выглядят как вызов функции ",".
Хоть это сначала и кажется непривычным, однако на практике благодаря простоте этого синтаксиса (что достигается с помощью уменьшения количества используемых специальных символов) привыкнуть можно быстро.
3. Терминологические замечания
Следует отдельно оговорить используемую терминологию. Основные термины на английском — quoting, unquoting, quaziquoting, splicing, macro expansion. В переводе книги Practical Common Lisp на русский язык для них используются слова «цитирование», «расцитирование», «квазицитирование» — и для последнего из них — «раскрытие макросов». Я не считаю этот вариант перевода удобным.
В данном материале будут использованы в качестве переводов «скрытие» для quoting, «раскрытие» для unquoting, «квазискрытие» для quaziquoting, «структурное раскрытие» для splicing, «расширение макроса» для macro expansion.
В приведённых далее примерах кода, можно увидеть синтаксис этих операций:
':: скрытие; применяется к последующей форме Hy; вместо её выполнения она будет обработанакак как данные.`:: квазискрытие; более сложная форма скрытия, позволяющая строить более сложные синтаксические структуры.~:: раскрытие; так как,занята в питоне для конструктора кортежей, используемый символ отличается от традиционной для Лиспа запятой. Употребляется в квазискрытой форме и помещает в неё результат выполнения следующей за ней формы.~@:: структурное раскрытие; работает аналогично предыдущей операции со следующим различием: результат оценки формы должен быть списком, и его элементы помещаются в объемлющую квазискрытую форму.
Выполнение обозначает вызов функции если форма — список, и доступ к значению символа в противном случае; литералы при выполнении остаются сами собой.
4. Суть метода
Получить конструкцию из hy как объект, с которым можно проводить манипуляции, можно при помощи скрытия. Расширение макросов само по себе не поможет — потому что макрорасширенный код сразу выполняется. Для того чтобы даже просто проинспектировать его расширение без скрытия не обойтись, например:
(macroexpand '(my-macro param1 param2 (do (print "hello!"))))
Поэтому можно сразу сосредоточиться на получении конструкций — например, генерируя их функциями на каких-то входных данных.
Тут нас ожидает несколько сложностей, о которых нельзя забывать.
- Скрытая конструкция сама по себе не обязана быть синтаксически корректной для самого
hy. В нашем случае корректность необходима. - Не все корректные конструкции
hyмогут быть транслированы в корректный код на питоне. В частности, это относится к именам переменных — правила на имена символов вhyгораздо расслабленнее.
При наличии грамотно сгенерированной кодовой конструкции в какой-либо переменной (например: результат вызова генерирующей функции), получить код на питоне можно, например, так:
(with [fd (open "some/python/file.py" "a")]
(.write fd "\n")
(.write fd (disassemble code True)))
5. Генерация имён
При генерации кода на питоне, в отличие, например, от написания макросов, для нас является важным, какие названия носят новые символы, т.е. в случае питона — имена вновь сгенерированных функций, классов, переменных. Другими словами, стандартный способ в Лиспе ((gensym)) нам не подходит. Также в hy нет стандартного для многих лиспов (intern), служащего для превращения произвольной строки (с поправкой на ограничения по грамматике) в символ.
К счастью, вся база кода hy доступна, и быстрым поиском мы убеждаемся, что (gensym) работает, создавая объекты HySymbol. Так же можем поступить и мы.
Следующий пример, несмотря на сказанное ранее — макрос.
(defmacro make-vars [data]
(setv res '())
(for [element data]
(setv varname (HySymbol (+ "var" (str element))))
(setv res (cons `(setv ~varname 0) res)))
`(do ~@res))
Помимо генерации названия переменной, в нём есть ещё одна полезная деталь. Это — сбор ��езультирующего AST из фрагментов путёи составления списка этих фрагментов, а затем раскрытия этого списка в нужном обрамлении.
6. Пример и замечания
При использовании hy для кодогенерации (в отличие от просто работы на нём), всплывают некоторые аспекты, которые при отправке кода на выполнение оказываются скрытыми.
В первую очередь это касается того, что в контексте AST и контексте выполнения одни и те же выражения обозначают разные вещи.
[ ]не просто список питона, аHyList;{ }открывает не словарь питона, аHyDict, и в внутренней моделиhyпредставлен как список;""не просто строковая переменная, а HyString.
и так далее. Основной вывод который можно из этого сделать таков: перечисленные (и другие) конструкции, будучи скрытыми, при дизассемблировании будут корректно преобразованы в соответствующие литералы python.
Для того, чтобы статически заполнить списки или словари в коде python, потребуется использование операции структурного раскрытия.
(setv class-def [`(defclass ~class-name [~(HySymbol (. meta-base __name__))]
[army_name ~army-name
faction_base ~(HyString faction)
alternate_factions [~@(map HyString alternate-fac-list)]
army_id ~army-id
army-factions [~@(map HyString army-factions)]]
(defn --init-- [self &optional [parent None]]
(apply .--init-- [(super ~class-name self)]
{~@(interleave (map HyString class-grouping)
(repeat 'True))
"parent" parent})
~@(map (fn [key]
`(.add-classes (. self ~(HySymbol key))
[~@(genexpr (HySymbol (. ut __name__))
[ut (get class-grouping key)])]))
class-grouping)))]))))
В приведённом примере производится заполнение списков в полях alternate_factions и army-factions объявляемого класса. Отметим, что в питоновском коде оба этих поля будут через нижнее подчёркивание. Заполнение производится на основе списков строк, поэтому применяется структурное раскрытие результата преобразования находящихся в переменных строк python в HyString.
Из приведённого фрагмента кода на hy можно сгенерировать следующий фрагмент кода на питоне:
class DetachPatrol_adeptus_ministorum(DetachPatrol):
army_name = u'Adeptus Ministorum (Patrol detachment)'
faction_base = u'ADEPTUS MINISTORUM'
alternate_factions = []
army_id = u'patrol_adeptus_ministorum'
army_factions = [u'IMPERIUM', u'ADEPTA SORORITAS', u'<ORDER>', u'ADEPTUS MINISTORUM']
def __init__(self, parent=None):
super(DetachPatrol_adeptus_ministorum, self).__init__(*[], **{u'heavy': True, u'troops': True, u'transports': True, u'hq': True, u'fast': True, u'elite': True, u'parent': parent, })
self.heavy.add_classes([Exorcist, Retributors, PenitentEngines])
self.troops.add_classes([BattleSisters])
self.transports.add_classes([ASRhino, Immolator])
self.hq.add_classes([Celestine, Canoness, Jacobus])
self.fast.add_classes([Dominions, Seraphims])
self.elite.add_classes([ArcoFlagellants, Assassins, Celestians, Dialogus, Hospitaller, Imagifier, Mistress, Priest, Repentia, PriestV2, Crusaders])
return None
Отдельно хотелось бы отметить как описан вызов конструктора родительского класса.
- Для функций из класса (которые начинаются на
.),applyтрактует первый позиционный аргумент, ему предоставленный (первый элемент списка, являющегося его вторым параметром) как объект, метод которого вызывается; - Можно производить заполнение словаря именованных аргументов при помощи структурного раскрытия;
- Для сопоставления каждому ключу (строке, преобразованной в
HyString) значения, применяетсяinterleave, которое производит итерацию по двум спискам, перемежая их элементы; - Символ
Trueподверженный скрытию, в кодеpythonбудет преобразован в себя; - В скрытой конструкции можно использовать нигде не объявленные (свободные) символы, которые будут преобразованы в переменные с такими же именами. Отметим; хоть в скрытой конструкции и находится объявление символа
parentкак параметра метода класса, во время выполнения функции, возвращающей скрытую кодовую конструкцию, такого символа не существует; - Можно генерировать серии однотипных операций из списков, производя структурное раскрытие списка скрытых конструкций
hy(пол��ченных преобразованием из исходного списка).
7. Использованные материалы
При написании данной статьи были использованы материалы из документации Hy и русского перевода Practical Common Lisp.
