Генерация кода на Python при помощи Hy
- пятница, 10 ноября 2017 г. в 03:12:44
Hy — диалект Лиспа, который встроен в питон.
Благодаря тому, что Hy трансформирует свой Лиспоподобный код в Абстрактное Синтаксическое Дерево (AST) питона, с помощью Hy весь прекрасный мир питона — на кончиках пальцев и в форме Лиспа.
Hy — своеобразный язык, похожий на каждого из своих родителей (больше, конечно, на Лисп). Для тех, кто не знаком с синтаксисом Лиспа, его можно в данном случае суммировать так.
Хоть это сначала и кажется непривычным, однако на практике благодаря простоте этого синтаксиса (что достигается с помощью уменьшения количества используемых специальных символов) привыкнуть можно быстро.
Следует отдельно оговорить используемую терминологию. Основные термины на английском — quoting, unquoting, quaziquoting, splicing, macro expansion. В переводе книги Practical Common Lisp на русский язык для них используются слова «цитирование», «расцитирование», «квазицитирование» — и для последнего из них — «раскрытие макросов». Я не считаю этот вариант перевода удобным.
В данном материале будут использованы в качестве переводов «скрытие» для quoting, «раскрытие» для unquoting, «квазискрытие» для quaziquoting, «структурное раскрытие» для splicing, «расширение макроса» для macro expansion.
В приведённых далее примерах кода, можно увидеть синтаксис этих операций:
'
:: скрытие; применяется к последующей форме Hy; вместо её выполнения она будет обработанакак как данные.`
:: квазискрытие; более сложная форма скрытия, позволяющая строить более сложные синтаксические структуры.~
:: раскрытие; так как ,
занята в питоне для конструктора кортежей, используемый символ отличается от традиционной для Лиспа запятой. Употребляется в квазискрытой форме и помещает в неё результат выполнения следующей за ней формы.~@
:: структурное раскрытие; работает аналогично предыдущей операции со следующим различием: результат оценки формы должен быть списком, и его элементы помещаются в объемлющую квазискрытую форму.Выполнение обозначает вызов функции если форма — список, и доступ к значению символа в противном случае; литералы при выполнении остаются сами собой.
Получить конструкцию из 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)))
При генерации кода на питоне, в отличие, например, от написания макросов, для нас является важным, какие названия носят новые символы, т.е. в случае питона — имена вновь сгенерированных функций, классов, переменных. Другими словами, стандартный способ в Лиспе ((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 из фрагментов путёи составления списка этих фрагментов, а затем раскрытия этого списка в нужном обрамлении.
При использовании 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
(полученных преобразованием из исходного списка).При написании данной статьи были использованы материалы из документации Hy и русского перевода Practical Common Lisp.