MacroGroovy — работа с AST на Groovy ещё никогда не была такой простой

    image
    Последнее время часто приходится работать с такой мощной возможностью Groovy как Compile-time AST Transformations.

    Так как я не люблю излишнюю динамику, то бОльшая часть проверок DSL на валидность у нас происходит на этапе компиляции, а так же мы используем очень много генерации кода. Поэтому каждый день приходится сталкиваться с составлением ASTNode-ов вручную.

    def someVariable = new ConstantExpression("someValue");
    def returnStatement = new ReturnStatement(
        new ConstructorCallExpression(
            ClassHelper.make(SomeCoolClass),
            new ArgumentListExpression(someVariable)
        )
    );
    


    До боли знакомые конструкции, не правда ли? Хотите, чтобы было вот так?
    def someVariable = macro { "someValue" }
    def returnStatement = macro { return new SomeCoolClass($v{ someVariable }) }
    


    Или даже так?
    def constructorCall = macro { new SomeCoolClass($v{ macro { "someValue" } }) }
    


    В данной статье речь пойдёт о моём решении этой проблемы, максимально близком к родному решению Groovy — github.com/bsideup/MacroGroovy



    AstBuilder

    Groovy 1.7 принёс такую замечательную, казалось бы, штуку как AstBuilder, который предлагает нам 3 способа построения AST:

    AstBuilder.buildFromString

    Передаём строку с кодом, на выходе имеем список ASTNode-ов:
    List<ASTNode> nodes = new AstBuilder().buildFromString("\"Hello\"")
    

    Преимущества
    • Входные данные — строка, может быть взята откуда угодно;
    • Не требует понимания как устроены ASTNode-ы;
    • Позволяет указывать CompilePhase;
    • Генерирует практически 100% валидный код;
    • Надёжный — не требует изменения в вашем коде если структура ASTNode-ов в Groovy поменялась.


    Недостатки
    • IDE не поможет вам с проверкой синтаксиса;
    • рефакторинги в IDE так же не будут работать;
    • Некоторые сущности создать не получится — например объявление поля класса.


    Часть этих недостатков призван исправить следующий метод.

    AstBuilder.buildFromCode

    Передаём замыкание (aka Closure) с кодом, на выходе имеем список нодов:
    List<ASTNode> nodes = new AstBuilder().buildFromCode { "Hello" }
    

    Преимущества (кроме преимуществ предыдущего метода)
    • IDE позволяет использовать autocomplete, проверку синтаксиса и рефакторинги в замыкании.

    Недостатки:
    • Данный способ не решает проблемы невозможности генерировать ряд сущностей;
    • Компилирует код, из-за чего не всегда получится использовать хитрые конструкции, либо не существующий класс;
    • Самый главный недостаток для меня: вызов buildFromCode требует чтобы метод вызывался именно путём создания AstBuilder-а:
      new AstBuilder().buildFromCode { ... }
      

      При этом Вы даже не сможете вынести AstBuilder в отдельное поле или локальную переменную (поэтому авторам Groovy даже пришлось прибегнуть к AstTransformation для этой AstTransformation чтобы не писать много кода)


    Для тех, кому не хватает обоих методов, есть третий способ:

    AstBuilder.buildFromSpec

    Данный метод принимает в себя замыкание (к слову, вы можете проголосовать за мою Issue или откомментировать Pull Request чтобы на этом методе появилась прекрасная аннотация DelegatesTo), которое представляет из себя DSL для построения AST:
    List<ASTNode> nodes = new AstBuilder().buildFromSpec {
        block {
            returnStatement {
                constant "Hello"
            }
        }
    }
    

    Преимущества
    • Позволяет использовать логику на Groovy для построения нодов;
    • Предоставлят возможность конструировать практически любую существующую ASTNode-у;
    • Важный плюс, т.к. тема AST generation в Groovy документирована не идеально: Полностью задокументирован и имеет обширные примеры использования в TestCase


    Недостатки
    • Иногда сложно понять что именно вам надо вызвать чтобы получить желаемый результат;
    • Менее многословен чем вызовы конструкторов нодов, но всё равно остаётся таковым;
    • Странная реализация — например некоторые методы принимают Class вместо ClassNode, что сводит его использование на нет;
    • Ненадёжен — AST может меняться с мажорными релизами языка;
    • Вы должны точно знать как ваш AST должен выглядеть в конкретной фазе компиляции;
    • IDE пока что (см. мой комментарий по поводу Pull Request-а) не поддерживают autocomplete для данного DSL.



    Комбинирование методов

    Так же стоит упомянуть что вы можете комбинировать эти методы:
    List<ASTNode> result = new AstBuilder().buildFromSpec {
        method('myMethod', Opcodes.ACC_PUBLIC, String) {
            parameters {
                parameter 'parameter': String.class
            }
            exceptions {}
            block {
                owner.expression.addAll new AstBuilder().buildFromCode {
                    println 'Hello from a synthesized method!'
                    println "Parameter value: $parameter"
                }
            }
            annotations {}
        }
    }
    



    MacroGroovy

    Итак, после столь обширного обзора возможностей, Вы можете спросить: Так на...*кхм*… фига нужен MacroGroovy?

    Рассмотрим пример из шапки поста:
    def someVariable = new ConstantExpression("someValue");
    def returnStatement = new ReturnStatement(
        new ConstructorCallExpression(
            ClassHelper.make(SomeCoolClass),
            new ArgumentListExpression(someVariable)
        )
    );
    

    Видите someVariable, переданную в конструктор списка аргументов? Поверьте мне, такая ситуация встречается очень и очень часто. И она сразу отметает buildFromCode и buildFromString. Значит остаётся только buildFromSpec, но вы помните список его недостатков? Вот тут и приходит на помощь MacroGroovy:

    def someVariable = macro { "someValue" };
    def returnStatement = macro { return new SomeCoolClass($v{ someVariable }) }
    


    Преимущества
    • Все преимущества первого и второго метода;
    • Не требует создание объекта, относительно которого вызывается метод macro — он доступен во всех классах как Extension Method
    • Внутри переиспользует код из AstBuilder, так что метод надёжный и протестированый;
    • Позволяет использовать логику Groovy внутри кода, т.к. $v принимает в себя замыкание, которое должно вернуть то, что надо поставить на место его вызова;
    • Очень, ОЧЕНЬ компактный:) сравните:
      macro { return mySuperVariable }
      и
      (new AstBuilder()).buildFromCode { return mySuperVariable }.first().expressions.first()
      


    Недостатки
    • К сожалению, поле класса вы с помощью macro {} так же не создатите;
    • Нет возможности CompilePhase;


    К слову, вы всё так же можете комбинировать buildFromSpec и macro:
    List<ASTNode> result = new AstBuilder().buildFromSpec {
        method('myMethod', Opcodes.ACC_PUBLIC, String) {
            parameters {
                parameter 'parameter': String.class
            }
            exceptions {}
            block {
                    owner.expression.addAll macro {
                            println 'Hello from a synthesized method!'
                            println "Parameter value: $parameter"
                    }
            }
    
            annotations {}
        }
    }
    


    Оставлю ссылку на тест, по которому видно как MacroGroovy уменьшает количество кода в разы:
    github.com/bsideup/MacroGroovy/blob/master/example/basicExample/src/test/groovy/ru/trylogic/groovy/macro/examples/basic/BasicTest.groovy

    Заключение

    Каждый из методов имеет свои плюсы и минусы, и я лишь постарался сгладить минусы других методов. Буду признателен за помощь в тестировании и ваши Pull Request-ы.

    Библиотека доступна в Maven Central, оставлю ссылку по которой всегда можно найти свежую версию:
    search.maven.org/#search%7Cga%7C1%7Cmacro-groovy

    Спасибо.
    Что предпочитаете Вы?

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

    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 11
    • 0
      Пользуясь случаем, призываю в топик jbaruch чтобы он рассказал что они используют и используют ли;)
      • +1
        Барух тебе щас нараскажет… )
      • 0
        Только меня напрягает фраза «не люблю излишнюю динамику» в статье про groovy?
        • 0
          Мы используем Groovy с @CompileStatic, это даёт высокую производительность, при этом фичи типа MOP и динамичного программирования мы итак не используем.

          Как понятно из статьи, даже DSL у нас статичные (но при этом не лишены своей краткости, гибкости и красоты:))
          • –1
            Разве груви без MOP не преврашается назад в яву?
            Или скорее во что-то типа xtend/xtext только с костылями типа описанного в статье
            • 0
              а Вы не пробовали изучить вопрос прежде чем оставлять комментарии в топиках?:)
              • 0
                Как раз пробовал.
                Но из-за своей внутренней культуры сразу не стал писать, что статья о том, из какого хлеба лучше делать троллейбусы.
                Вы взяли динамический язык, потом решили не использовать в нем всё, кроме одной, вчера появившейся, фичи, чтобы получить статический ДСЛ. И статья в духе «гляньте, как круто». Нет, не круто, инструменты надо подбирать под задачу.
                • 0
                  1) MOP — это лишь одна из немногих фич языка, без которой спокойно живётся
                  2) AST transfomations — вчера появившаяся фича? Расскажите это всем поклонникам AOP например:)
                  3) опять же, это делалось мягко говоря не только ради дсл, точнее совсем не для них
                  4) инструмент был выбран под свою конкретную задачу и с ней он справляется на отлично:)

                  так что могу сделать вывод что вы тупо тролль:) А то, что тролль не умный — подтверждает ваша карма;)
                  • 0
                    @CompileStatic — вчера появившаяся фича.
                    Если в груви вам понадобились трансформации AST больше пары раз, значит что-то вы делаете не так.
                    Добавите такой пункт в опрос и узнаем, кто из нас не умный.
                    • 0
                      Практически в каждом Groovy проекте используются трансформации. ToString, EqualsAndHashCode, Delegate, Category, Mixin и так далее. Всё это AST трансформации, если вы вдруг не знали.
                      • 0
                        Ладно, кажется пора завязывать спорить. Обилие коментариев к статье на мой взгляд очень хорошо показывает, скольким людям необходимо делать свои AST-трансформации в груви. Всем хватает динамических DSL в динамическом языке. Для статических люди просто выбирают другие инструменты.

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