Статьи

Как может выглядеть статически типизированное метапрограммирование


Я хочу начать с фрагмента кода и предложить читателю понять, что означает этот код.
Чтобы сделать задачу немного честной, я говорю вам, что это полное содержимое файла с именем …
/grails-app/controllers/gppgrailstest/WebSocketChatController.groovy , который, очевидно, является частью некоторого приложения Grails.

def chatService

index: {
def id = request.session.id
[
sessionId: id,
userName: request.session.userName ?: (request.session.userName = chatService.newUserName())
]
}


Ладно, действительно сложно долго сохранять интригу, и приведенный ниже код является точной копией приведенного выше сценария после некоторых преобразований AST, которые поддерживает Grails.

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

package gppgrailstest

@Typed class WebSocketChatController implements org.mbte.grails.languages.ControllerMethods {
gppgrailstest.ChatService chatService

static def defaultAction="index"

Closure index = {
def id = request.session.id
[
sessionId: id,
userName: request.session.userName ?: (request.session.userName = chatService.newUserName())
]
}
}

На первый взгляд, наша трансформация не сделала ничего нетривиального, но она многое сделала. Точнее:

  • мы добавили объявление пакета, вычтя имя пакета из соглашения Grails
  • мы превратили оригинальный скрипт в класс контроллера
  • объявление скрипта локальной переменной верхнего уровня ‘chatService’ стало свойством ‘chatService’
  • мы обнаружили, что наше приложение Grails содержит служебный бин с именем chatService типа gppgrailstest.ChatService и поняли, что свойство ‘chatService’ будет введено с этим бином
  • мы преобразовали помеченное выражение блока в индекс свойства типа Closure
  • мы поняли, что, поскольку у нас есть только одно действие в нашем контроллере, оно будет по умолчанию
  • мы добавили аннотацию @Typed к нашему классу контроллера, указав, что он должен компилироваться статически
  • мы добавили интерфейс черты org.mbte.grails.languages.ControllerMethods в наш контроллер

Я могу согласиться, что само преобразование AST не было слишком сложным. На самом деле полный код для преобразования составляет менее 110 строк Groovy ++. Что далеко не тривиально, так это то, что наш код действительно может быть статически скомпилирован.

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

  • что мы подразумеваем под свойством «запрос»
  • даже если мы поняли, что «request» относится к типу HttpServletRequest, а «request.session» относится к типу HttpSession, а «request.session.id» относится к типу String, все еще не ясно, что означает «request.session.userName»

Второй вопрос немного проще, поэтому мы начнем с него. В предыдущей статье Groovy ++ в действии: статически типизированная динамическая диспетчеризация мы обсуждали мощную технику обработки неразрешенных методов и свойств компилятором Groovy ++. Все, что нам нужно сделать, это добавить методы getUnresolvedProperty и setUnresolvedProperty в наш класс.

Если HttpSession был классом (в отличие от интерфейса), и мы могли бы изменить этот класс (что мы никак не можем), то все, что нам нужно сделать, это добавить два следующих метода.

    def getUnresolvedProperty(String name) {
getAttribute(name)
}

void setUnresolvedProperty(HttpSession session, String name, Object value) {
setAttribute(name, value)
}


Поскольку HttpSession является интерфейсом, нам нужно использовать методы расширения (или категории)

    static def getUnresolvedProperty(HttpSession session, String name) {
session.getAttribute(name)
}

static void setUnresolvedProperty(HttpSession session, String name, Object value) {
session.setAttribute(name, value)
}

Методы расширения или категории являются статическими методами, определяющими дополнительные методы для некоторого другого типа. Расширяемый класс определяется типом первого параметра, а параметры добавляемых методов определяются остальными параметрами.

В Groovy ++ есть три способа сделать методы расширения применимыми во время компиляции

  • @ Используйте аннотацию
  • статические методы, определенные в скомпилированном классе или его суперклассах
  • глобально с помощью специально названного файла регистрации в пути к классам

Groovy ++ предоставляет множество методов расширения для различных классов, связанных с сервлетами.


Итак, мы закончили с request.session.userName и, используя статически типизированные методы динамической отправки и расширения, теперь он работает как атрибут сеанса.

А как насчет свойства ‘request’ и всех других методов и свойств, которые Grails обычно предоставляет контроллеру посредством динамического метапрограммирования во время выполнения? Нам нужно сделать все эти методы доступными в классе контроллера (и затем в закрытии действия). Есть несколько способов добиться этого

  1. Мы можем попытаться ввести общий суперкласс для всех контроллеров и вывести наш контроллер из этого суперкласса. Это не так хорошо, если мы хотим повторно использовать некоторые контроллеры по наследству.
  2. Мы можем ввести пустой интерфейс маркера и определить некоторые методы расширения для этого интерфейса, как мы делали раньше. Это лучше, но мы не можем переопределить метод в нашем контроллере.
  3. Мы можем использовать черты.

Особенностью является Groovy ++ (концепция заимствована из Scala), и это интерфейс с реализациями некоторых методов по умолчанию.

Если статически скомпилированный класс реализует такой интерфейс, но не обеспечивает собственную реализацию такого метода, реализация по умолчанию будет создана автоматически.


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

Вуаля. У нас есть полностью статически типизированный контроллер. Никакой черной магии.

Надеюсь это было интересно. Вы можете узнать больше на странице проекта Groovy ++.

Спасибо за чтение и до следующего раза.