Статьи

О статической компиляции Groovy


Groovy — отличный язык программирования.
Период. Я не собираюсь спорить об этом и не собираюсь никого убеждать. Если вы не разделяете это мнение или вам нужны дополнительные аргументы, посетите очень дружелюбное сообщество по адресу http://groovy.codehaus.org.

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

Следующий «манифест» запускает главную страницу веб-сайта Groovy. Пожалуйста, прочитайте это:

Groovy .. .

  • это гибкий и динамический язык для виртуальной машины Java

  • опирается на сильные стороны Java, но имеет дополнительные мощные функции, вдохновленные такими языками, как Python, Ruby и Smalltalk

  • делает современные возможности программирования доступными для разработчиков Java с почти нулевой кривой обучения

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

  • облегчает написание сценариев оболочки и сборки с помощью мощных примитивов обработки , возможностей OO и Ant DSL

  • повышает производительность труда разработчиков за счет сокращения кода скаффолдинга при разработке веб-приложений, графических интерфейсов, баз данных или консольных приложений

  • упрощает тестирование , поддерживая модульное тестирование и макетирование из коробки

  • легко интегрируется со всеми существующими объектами и библиотеками Java

  • компилируется прямо в байт-код Java, так что вы можете использовать его везде, где вы можете использовать Java

Все это правда. Я могу поставить свою подпись под каждым словом здесь, и как один из разработчиков Groovy Core и соучредитель первой в истории Groovy компании G2One, Inc. (в настоящее время являющейся частью SpringSource и подразделением VmWare), я много положил усилий, чтобы это произошло.

Этот манифест не говорит, и очень важно отметить, что, как знают все разработчики Groovy, нужно заплатить определенную цену.

  • нет проверки времени компиляции
  • прирост производительности от 5 до 15 раз по сравнению с кодом, написанным на Java или Scala

В общем, уравнение, которое мы имеем:

Отличные функции (в том числе динамические) => Медленно и без проверки времени компиляции

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

Я не хочу вдаваться в эту дискуссию (особенно как ответственную за большую часть прироста производительности). Я думаю, что это почти контрпродуктивно. Я только хочу упомянуть, что, например, по причинам производительности Groovy сегодня практически бесполезен для многоядерного программирования (даже с библиотекой Brillian GPars — раскрытие: я один из разработчиков GPars).

То, о чем я хочу поговорить, это:

Стоит ли платить какую-либо цену?

И если есть такая цена, то что и за что мы должны платить?

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

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

Хорошо, вполне понятно … Но почему? Поскольку прекрасные возможности метапрограммирования Groovy во время выполнения могут изменить поведение любого метода (формально говоря, каждый вызов на каждом сайте вызовов) Технически это означает, что если мы обнаружили (и знали это наверняка), что в какой-то момент мы вызовем существующий метод класс с правильно введенными аргументами, мы все еще не можем использовать байт-код, вызывающий этот метод, и даже не можем быть уверены, что возвращаемое значение будет иметь ожидаемый тип.

Подумайте об этом на секунду:

Метапрограммирование во время выполнения требует динамической отправки

  • Динамическая диспетчеризация не позволяет проверять время компиляции

  • Динамическая отправка медленная

Все вместе мы платим огромную цену только за одну (очень важную) особенность языка. Но все же … только для одной функции.

Представьте на секунду, что мы не используем метапрограммирование во время выполнения в каком-то фрагменте кода, и у нас есть ключевое слово или аннотация, чтобы пометить такой фрагмент (метод или класс или весь модуль или расширение файла) для статической компиляции. Решает ли это нашу проблему?

Я утверждаю, что это делает:

  • Для Groovy можно написать статически типизированный компилятор, сохраняющий все вкусности красивого языка.

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

  • Возможно, у него будет метагрограммирование во время компиляции (свойства, категории, стандартные методы groovy, миксины, преобразования ast — все, кроме метапрограммирования во время выполнения)

  • Можно поддерживать черты типа Scala (также известные как интерфейсы с реализацией по умолчанию), что является чрезвычайно мощным инструментом метапрограммирования.

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

  • Возможно даже, что из-за логического вывода статически скомпилированный код может быть немного менее подробным по сравнению с Groovy (например, почти нет необходимости в преобразовании «как»)

 
Есть несколько причин, по которым я утверждаю:

  • Это не ракетостроение
  • Проверенное решение Scala для части вывода типа
  • В настоящее время проводится определенная работа, подтверждающая каждое из утверждений, изложенных выше.

Позвольте мне поделиться с вами некоторыми фрагментами прокомментированного кода:

/**
Trait Function1 defines interface of abstract function with one argument.

It also additionally provide implementation of several useful methods
Traits are always statically compiled
*/
@Trait
abstract class Function1<T,R> {

/**
Body of the function
*/
abstract R apply (T param)

/**
Creates another function applying another function provided as argument to result of calculation of this one
*/
public <R1> Function1<T,R1> andThen (Function1<R,R1> g) {
/*
here is type inference comes in to play
Groovy allows us not to specify return explicitly AND
compiler knows that return type is Function1 (interface with one abstract method
'apply' because the rest methods have default implementation),
so it can compile the whole new class and return of instance of this class

It is very interesting to notice that we don't even need
to specify type of parameter 'arg' compiler is smart enough to deduct it

Another interesting thing to notice is use of [] operator. As we define
default implementation of getAt method (Groovy convention for []) we can use it
And the last but also important note: In pure Groovy we would write {...} as Function1. Static compiler has enough information to allow us to skip 'as'
*/
{ arg -> g[ apply(arg) ] }
}

/**
Creates another function applying this one to result of another function
provided as argument
*/
public <T1> Function1<T1,R> composeWith (Function1<T1,T> g) {
/*
very similar to previous method
*/
{ arg -> apply(g [arg] }
}

/**
Provides [] - syntax for Function1
*/
R getAt (T arg) {
apply(arg)
}
}

Теперь мы можем попробовать использовать этот код:

    /*
Method to convert iterator to another iterator applying given function to every element
*/
static <T,R> Iterator<R> map (Iterator<T> self, Function1<T,R> op ) {
[ next: { op[self.next()] }, hasNext: { self.hasNext() }, remove: { self.remove() } ]
}

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

   /*
Method to conver iterator to another iterator applying given function to every element
*/
static <T,R> Iterator<R> map (Iterator<T> self, Function1<T,R> op ) {
/*
Again as above compiler has enough information to understand that provided map expression should be compiled in to instantiation of a new class
*/
[
// no need to specify return types of methods because compiler knows
// also compiler knows that next () accep no arguments, so correct method will be created
// op [] again at our service
next: { op[self.next()] },

hasNext: { self.hasNext() },

remove: { self.remove() }
]
}


ОК, мы почти у цели. Давайте проверим, чего мы достигли:

        /*
we don't define type of iter variable
isn't it obvious that it is Iterator<String>?

Look:
[0, 1, 2] is ArrayList<Integer>
[0, 1, 2].iterator () is Iterator<Integer>
map {...} called with Function1<Integer,String> (String is return type of provided closure as well as 'it' parameter is Integer)
*/
def iter = [0, 1, 2].iterator().map { (it + 5).toString() }
def res = []
// we use normal Groovy Truth but staticly compiled. So iter instead of iter.hasNext ()
while (iter) {
// and yes, toUpperCase method is at our service
res << iter.next ().toUpperCase()
}
assertEquals (["0", "1", "2"], res )

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

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

Дополнительная статическая компиляция для Groovy может сделать мир еще лучше!

Давайте сделаем это