[Из песочницы] Генерация кода на Python при помощи Hy

1. Что такое Hy


Hy — диалект Лиспа, который встроен в питон.


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


image



2. О синтаксисе Hy, очень кратко


Hy — своеобразный язык, похожий на каждого из своих родителей (больше, конечно, на Лисп). Для тех, кто не знаком с синтаксисом Лиспа, его можно в данном случае суммировать так.


  1. Отступ не играет роли. Вместо этого — уровни вложенности в выражения из круглых скобочек.
  2. Во всех вызовах функций название функции попадает в скобки со списком аргументов на первое место; запятые в списке аргументов не используются.
  3. Все операторы записываются так, как будто они — функции.
  4. Двоеточия не используются.
  5. Литералы для строк и словарей работают как и раньше; строки записываются в двойных кавычках, кортежи выглядят как вызов функции »,».


Хоть это сначала и кажется непривычным, однако на практике благодаря простоте этого синтаксиса (что достигается с помощью уменьшения количества используемых специальных символов) привыкнуть можно быстро.


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!"))))


Поэтому можно сразу сосредоточиться на получении конструкций — например, генерируя их функциями на каких-то входных данных.


Тут нас ожидает несколько сложностей, о которых нельзя забывать.


  1. Скрытая конструкция сама по себе не обязана быть синтаксически корректной для самого hy. В нашем случае корректность необходима.
  2. Не все корректные конструкции 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'', 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.

© Habrahabr.ru