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'<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.