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.

Комментарии (28)


  1. werevolff
    09.11.2017 19:48

    Есть хоть одна разумная причина делать всё это?


    1. MikeLP
      09.11.2017 20:47

      Нет.


      1. resetme
        09.11.2017 22:37

        Есть хоть одна разумная причина НЕ делать всё это?


        1. werevolff
          10.11.2017 06:37

          Есть. Lisp — это один из первых высокоуровневых языков. Пайтон — один из современных. По скорости исполнения они сопоставимы. Впрочем, сегодня Lisp всё чаще считают медленным. Синтаксис Python, в большей степени, проще, чем синтаксис Lisp. Код — более читаемый. Собственно, простота, удобство и скорость написания кода — главная фишка высокоуровневых языков. Lisp немного устарел в этом плане, хотя используется в прикладных задачах. Но, не думаю, что автор брался за замену Lisp кода Python'ом.
          Итого: у нас есть два высокоуровневых языка с динамической типизацией, сопоставимых по скорости выполнения программ. Язык, который получается после трансляции, более прост и визуально понятен, чем язык, который используется для трансляции.
          Вывод: делать такую трансляцию — неразумно.


          1. Kurvivor19 Автор
            10.11.2017 08:07

            Начнём с того что использованными диалект лиспа младше питона, и был написан не просто позже питона а на питоне. Скорость выполнения у них совершенно одинаковая, система типизации также. Мне незачем защищать сам принцип трансляции в питон — раз это было сделано, значит это кому-то нужно; мне, например пригодилось.
            А вот «синтаксис питона проще» — чистая неправда


            1. werevolff
              10.11.2017 08:10

              О том и речь: два медленных языка. Только Lisp с его диалектами выглядит в рамках задачи уж больно оверхедом.

              мне, например пригодилось.

              Значит, вы что-то делаете не так.


            1. Alesh
              10.11.2017 10:08

              мне, например пригодилось

              увидел)


    1. Kurvivor19 Автор
      09.11.2017 21:45

      Я занялся этой задачей из практических соображений. Не хотелось писать много однотипных классов вручную.


      1. SirEdvin
        09.11.2017 22:31

        Что мешает генерировать эти классы при помощи python?


        1. Kurvivor19 Автор
          09.11.2017 22:44

          Думаю, ничего. Вопрос в удобстве. Т.е. надо бы спросить «что помогает». В hy помогает синтаксис, принцип на котором построен язык и наличие готовых классов и функций под задачу.


          1. SirEdvin
            10.11.2017 00:22

            У меня просто была похожая задача и в целом я успешно решил ее через метаклассы + генерация кода.
            Возможно, вашу задачу было можно так же решить?


            1. werevolff
              10.11.2017 05:32

              Есть миксины. Есть магическая ф-ия type, которую можно использовать как фабрику классов.


              Генерация python кода с помощью lisp может пригодиться… Никогда?


            1. Kurvivor19 Автор
              10.11.2017 10:49

              Вполне вероятно. Можете рассказать, как именно вы генерировали код?


              1. werevolff
                10.11.2017 11:37

                На своей любимой механической клавиатуре. Как же ещё?

                Нужно, для начала, рассмотреть вашу задачу. Почему я говорю, что вы что-то делаете не так: если речь идёт о генерации классов во время исполнения, то есть замечательная функция type(). Она позволяет создавать класс непосредственно в процессе выполнения кода. Т.е. вы пишете фабрики, которые, используя type, формируют классы с нужными методами. В input приходят аргументы, запускается генератор, возвращает вам класс и инстанс, с которым вы уже можете работать. Вот вам и DSL.

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

                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])
                


                Вот это что за трёхколёсник? Не совсем понятно, что оно делает, но если бы мне нужно было собрать отряд, я бы начал вот с чего:

                class PriestMixin(object):
                    def heal_one(target):
                        # do something
                        pass
                    
                    def heal_many(targets):
                        for target in targets:
                            self.heal_one(target)
                
                class WarriorMixin(object):
                    def kick_ass(target):
                        # kick ass
                        pass
                
                class KnightMixin(object):
                    def protect(target):
                        # protect target
                        pass
                
                class Patrol(object):
                    def __init__(self, id, name, containments):
                        self.id = id
                        self.name = name
                        self.unit_classes = [type(cont['name'], (*cont['mixins']), {}) for cont in containments]
                        self.containment = [unit_class() for unit_class in unit_classes]
                
                Patrol('SuicideSquad', 'Suicide Squad', [
                    {'name': 'VasyaTheKnight', 'mixins': [PriestMixin, KnightMixin]},
                    {'name': 'YouraMotherFucker', 'mixins': [WarriorMixin]}
                    ])
                


                1. Kurvivor19 Автор
                  10.11.2017 13:09

                  Во-первых, про способ генерации я спрашивал не вас, а SirEdvin. Потому что о нём говорил он. Нужно было указать его имя с ником? Извините за неясность
                  Во-вторых, я прекрасно знаю про миксины и пользуюсь ими. Собственно, модель множественного наследования в питоне приводит меня в полное восхищение и щенячий восторг своей удобностью.
                  В-третьих, речь не шла о динамической генерации классов во время выполнения. В данном случае речь идёт об автоматической генерации кода который будет выполняться когда-то потом. Наличие сгенерированного кода позволяет в дальнейшем его ручную правку, применение к нему инструментов контроля версий — ну и проще в отладке чем динамически сгенерированные типы. Во всяком случае, этим руководствовался я.
                  Спасибо за совет касающийся приведённого мной примера


                  1. werevolff
                    10.11.2017 14:04

                    Ну, генерация кода программой — не новая фишка. Она прекрасно используется, в том числе, во вредоносном ПО. К сожалению, опять же, не совсем понятно для чего это делать. Если речь идёт об ИИ, который будет писать код, то это одно. Если нужно сделать червя, который будет самостоятельно дописывать код — другое. В первом случае, согласен, Lisp вполне может работать, как часть ИИ и использоваться для замены программистов (четвертовать бы вас за ваши исследования, или на кол посадить).

                    Во втором случае, лучше обратить внимание на непосредственно компилируемые языки, на которых можно написать небольшое приложение, которое будет прекрасно исполняться в различных средах и дописывать вредоносный код.


    1. DollaR84
      09.11.2017 21:45

      Мне вот тоже интересно, какая практическая польза писать один язык программирования через другой? Кроме теории что это сделать можно.


      1. Kurvivor19 Автор
        09.11.2017 21:48

        Для удобства, разумеется. Вы ведь не пишете на ассемблере когда можно писать на С?
        Ну а лиспы в целом довольно часто в другой язык компилируются, в джакузи и Джаваскрипт например


        1. werevolff
          10.11.2017 05:45

          Это так же глупо, как компилировать CSS в scss. Ладно, с компиляцией lisp в js лет пять назад можно было согласиться. Но лисп в Пайтон?! Лисп менее лаконичен и по скорости выполнения может уступать python. К тому же, он хуже читается.


          1. Kurvivor19 Автор
            10.11.2017 08:09

            По старости выполнения этот лист пайтону уступить вряд ли сможет. Такова уж его природа что скорость выполнения будет всегда одинакова


        1. x86128
          10.11.2017 08:09

          Можно ли на считать этот лисп инструментом для создания разного рода DSL-ей для Python?


          1. Kurvivor19 Автор
            10.11.2017 08:13
            +1

            Да; это, пожалуй его самое лучшее применение.


          1. werevolff
            10.11.2017 08:33
            -1

            Википедия говорит, что это основная область применения Hy. Однако, практическая сторона хромает. Например, Hy, по факту, весь код будет переводить в медленный python. Большинство полученных сниппетов будут примерно одинаковы по размеру в строках. С тем лишь исключением, что в Hy будет целая куча скобок.

            О том, что Hy позволяет упростить часть синтаксиса пайтона, говорить нельзя: он полностью рассчитан на те же самые импорты, подключение библиотек python и их использование. Результирующий код будет выполняться python, и это серьёзно ограничивает возможности Hy.

            Разумеется, можно использовать Hy для разработки DSL, рассчитанных на применение различными специалистами, которые не будут учить python. Только вот в чём проблема: Lisp в плане синтаксиса сложнее, чем python. Писать DSL для применения одним Lisp-гуру на всю компанию — экономически не выгодно. Создание же своего синтаксиса DSL проще реализовать на Python.

            Да, посмотрел я в гитхабе репу этого Hylang:

            Well. Python is awesome. So awesome, that we have so many tools to alter the language in a core way, but we never use them.

            Why?

            Well, I wrote Hy to help people realize one thing about Python:

            It's really awesome.

            Oh, and lisps are neat.


            Иными словами, Lisp аккуратен, но Hylang был создан для того, чтобы показать, что пайтон лучше :-D Именно поэтому надо сперва читать README, а потом уже пилить обзоры.


      1. werevolff
        10.11.2017 05:39

        Если бы эти языки имели разницу, смысл был бы. Например, если бы python генерировал c++. Это можно было использовать для увеличения производительности в отдельных вычислениях.


        Либо, если нам нужно сгенерировать более сложный код с помощью более простого. Например, контроллер поддерживает golang. Но python — лаконичнее, и мы пишем код на python, который транслируется в go.


        Но вот использовать устаревший тормознутый высокоуровневый язык, популярный в ИИ, чтобы генерировать код на современном и лаконичном тормознутом языке, популярном в ИИ — верх неблагоразумия.


        1. seryh
          10.11.2017 12:29
          -1

          Смысл есть, «Hy» имеет распространенный диалект лиспа. Автор теперь может писать свой тормознутый высокоуровневый код и компилировать его в быстрый JVM байт-код (Clojure), js на фронте (ClojureScript), NodeJs (lumo), C++ (ferret-lang) и многое другое. Сложность синтаксиса вещь субъективная, после преодоления порога входа (да большой), писать на лиспах — быстро, просто и приятно. Читать чужие исходники на диалектах лиспа с иммутабельностью (Clojure), выходит еще проще чем документацию. Ну и бодрости к скорости разработки добавляет мощный REPL (если есть под диалект).


          1. aleksponom
            10.11.2017 20:07

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

            Но блин Hy, с этой компиляцией в AST, это что-то совсем не то. Интероп есть. Но когда пытаешься что-то сделать, то понимаешь, что проще это сделать сразу на питоне. Может быть все дело в сырости и незрелости. Сами по себе скобочки особых преимуществ не дают. Мультиметоды, например, в Hy в каком-то пред-альфа состоянии.

            Скорее, наверное, lumo с нодой быстрее допилят и доведут до пристойного вида.


  1. Deepwalker
    10.11.2017 12:12

    Вероятно можно было бы сделать на Hy прикольную кодогенерацию. Но возможно пример хромой, и галопом по европам тоже не помогает. Может вторая попытка?


    1. Kurvivor19 Автор
      10.11.2017 13:10

      Будет учтено