Статьи

Groovy ++ в действии: статически типизированная динамическая отправка

Уже стало общепринятым утверждать, что как динамически, так и статически типизированный код имеет свои достоинства и недостатки. Сборщики Groovy (xml, разметка и т. Д.) Являются огромным примером, когда динамическая диспетчеризация выигрывает по сравнению с статически типизированным кодом с точки зрения выразительности (за исключением того, что смешать типизированный и нетипизированный код нелегко), но теряет производительность и статические проверки. Сегодня я собираюсь показать, как мы можем построить статически типизированный компоновщик с Groovy ++, не платя такие штрафы.

Groovy ++ уже содержит уникальную возможность комбинировать статически и динамически типизированный код вместе. Если вы аннотируете свой метод (или класс или весь скрипт) с помощью @Typed (TypePolicy.MIXED), то компилятор не будет жаловаться на неразрешенные методы и свойства, а вместо этого будет генерировать динамические вызовы, которые будут отправляться через мета класс.

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

Давайте начнем с примера, вдохновленного Groovy Enhancement Proposal 7 — поддержка JSON .

Мы хотим встроить и вывести на стандартный вывод JSON представление личных данных человека.

JsonClosure externalData = {
additionalData {
married true
conferences(['JavaOne', 'Gr8conf'])
projectRoles([
'Groovy' : 'Despot',
'Grails' : 'Commiter',
'Gaelyk' : 'Lead'
])
}

whaeverElseData (["xxxxxx", [x: 12, y:14], ['a', 'b', 'c']])
}

JsonBuilder2 builder = [new PrintWriter(System.out)]
builder.person {
firstName 'Guillaume'
lastName 'Laforge'
address (
city: 'Paris',
country: 'France',
zip: 12345,
)

externalData ()
}

И здесь ожидается выход JSON

{
"person" : {
"firstName" : "Guillaume",
"lastName" : "Laforge",
"address" : {
"city" : "Paris",
"country" : "France",
"zip" : 12345
},
"additionalData" : {
"married" : true,
"conferences" : [ "JavaOne", "Gr8conf" ],
"projectRoles" : {
"Groovy" : "Despot",
"Grails" : "Commiter",
"Gaelyk" : "Lead"
}
},
"whaeverElseData" : [ "xxxxxx", {
"x" : 12,
"y" : 14
}, [ "a", "b", "c" ] ]
}
}

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

Это удивительно просто: когда компилятор находит вызов метода, который он не может разрешить, он начинает искать метод со специальным именем invokeUnresolvedMethod, который имеет абсолютно любой возвращаемый тип и абсолютно любые параметры, если первый параметр имеет тип String.

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

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


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

Давайте посмотрим, как это работает. Наша реализация JsonBuilder удивительно проста

class JsonBuilder2 {
protected static ThreadLocal current = []

private final MappingJsonFactory factory = []
private final JsonGenerator gen

JsonBuilder2(Writer out) {
gen = factory.createJsonGenerator(out)
gen.useDefaultPrettyPrinter()
}

void call(JsonClosure obj) {
try {
current.set(gen)

gen.writeStartObject()
obj ()
gen.writeEndObject()
gen.close ()
}
finally {
current.remove()
}
}

void invokeUnresolvedMethod(String name, JsonClosure obj) {
call {
gen.writeObjectFieldStart name
obj ()
gen.writeEndObject()
}
}
}

Внизу мы используем блестящий каркас Джексона для обработки JSON.

Тот, кто не знаком с Groovy / Groovy ++, может заметить, как легко использовать существующие библиотеки Java.


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

builder = [new PrintWriter(System.out)]
builder {
person {
firstName 'Guillaume'
lastName 'Laforge'
address (
city: 'Paris',
country: 'France',
zip: 12345,
)

externalData ()
}
}

Очень важно отметить, что существует огромная разница между первой и второй формой.

builder.person {…} означает builder.invokeUnresolvedMethod («person», (JsonClosure) {…}) и

builder {…} означает builder.call ((JsonClosure) {…})

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

abstract class JsonClosure {

protected JsonGenerator gen

abstract void define ()

void call () {
if(!gen)
gen = JsonBuilder2.current.get()

if(!gen)
throw new IllegalStateException("Can't use JsonClosure outside of JsonBuilder")

define ()
}

void invokeUnresolvedMethod(String name, Object obj) {
if(obj == null) {
gen.writeNullField name
return
}

switch(obj) {
case Closure:
gen.writeObjectFieldStart(name)
obj.call()
gen.writeEndObject()
break

case JsonClosure:
gen.writeObjectFieldStart(name)
obj.gen = gen
obj.define()
obj.gen = null
gen.writeEndObject()
break

case String:
gen.writeStringField(name, obj)
break

case Number:
gen.writeNumberField(name, obj)
break

case Map:
gen.writeObjectFieldStart(name)
for(e in obj.entrySet()) {
invokeUnresolvedMethod(e.key.toString(), e.value)
}
gen.writeEndObject()
break

case Iterable:
gen.writeArrayFieldStart(name)
iterate(obj)
gen.writeEndArray()
break

case Object []:
invokeUnresolvedMethod(name, obj.iterator())
break

case Boolean:
gen.writeBooleanField(name, obj)
break

default:
gen.writeObjectField(name, obj)
break
}
}

void iterate(Iterable obj) { ........... }
}

Это почти все. Конечно, более вовлеченные сборщики могут также использовать getUnresolvedProperty (String) и setUnresolvedProperty (String, SomeType), который позволяет преобразовывать неразрешенный доступ к свойствам в быстрые динамические вызовы (эта функция еще не реализована в транке)

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

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