Pnuts

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 21:27, 8 июня 2016.
Pnuts
Парадигма Объектно-ориентированный, скриптовой
Разработчики Toyokazu Tomatsu (Sun Japan)
Печать дисциплины статическая, динамическая, утиная
Платформа JVM
Лицензия Sun Public License
Портал: https://pnuts.dev.java.net/

Поддержка объектно-ориентированного программирования в Pnuts

В течение нескольких лет я пытался понять, как объектно-ориентированная функция должна быть интегрирована в процедурном языке с лексической областью видимости. В последних обновлениях Pnuts, инфраструктура его класса была переразработана на основе более сложной внутренней архитектуры, чем в в нескольких попытках, которые я предпринял в предыдущих версиях. В этой статье я объясню, как ОО функциональность была интегрирована в Pnuts.

Классы в Pnuts

Например, просто счетчик может быть определен в Pnuts следующим образом.

class Counter {
  i = 0
  inc(){ i++ }}

Класс компилируется в обычный класс Java. Никакой разницы от Java классов не будет после компиляции и загрузки. Атрибуты могут быть напечатаны, а могут и не быть. В приведенном выше примере, переменная i представляет собой ненапечатанный атрибут. Методы получения и установки, в этом примере get() и set(), неявно определяются для каждого атрибута.

Предполагается, что определение класса следует конвенции именования Java Beans. Методы получения и установки вызываются, когда читается или записывается атрибут из скрипта.

c = Counter()
c.i      ---> 0
c.i = 1
c.i      ---> 1

Возврат типа и параметрических типов метода являются необязательными. Если тип не указан, Object.class используется неявно, кроме случаев, когда метод переопределяет существующий метод, определенный в суперклассе; в этом случае он наследует метод подпись.

Отсутствие доступа контроля может быть указано для методов и конструкторов. Методы и конструкторы всегда public.

Ключевые слова могут быть использованы в Pnuts так же, как и в Java. Например, следующий код иллюстрирует, как настроить метод установки.

class MyCounter extends Counter {
  setI(i){
     if (this.i != i) println("changed")
     super.setI(i)
   }
  inc(){ setI(this.i+1) }}

Следующий код - еще один пример, который является подклассом HashMap, что автоматически создает набор для не сопоставленного ключа.

class MyMap extends java.util.HashMap {
  get(key){
     if ((v = super.get(key)) == null){
       super.put(key, v = new java.util.HashSet())
     }
     v
  }}

Загрузка класса

Классы, определенные в Pnuts могут быть загружены двумя разными способами. Во-первых, файлы скриптов с расширением .pnc могут быть автоматически скомпилированы и загружены с помощью PnutsClassLoader, как если бы это были фалы типа .class, которые могут быть загружены системным классом loader. Так как зависимость между классами автоматически решается загрузчиком классов, вероятность возникновения ошибки повторного определения уменьшается, в сравнении с другими вариантами. Кроме того, определение класса может быть встроено в файл сценария и выполнен в качестве обычного файла сценария. В этом случае, классы временно определены в сценарии, то есть объем имени класса внутри файла сценария, который содержит определение класса. Когда скрипт ‘A’ выполняет скрипт ‘B’, эти скрипты используют различные загрузчики классов для загрузки встроенных классов.

Реализация

Когда класс счетчик скомпилирован, компилятор генерирует класс skelton указанной подписи. В то же время, компилятор создает функцию, которая создает замыкание. Функция возвращает массив функций, каждая из которых связана с методом класса. Атрибуты отображаются в локальных переменных функции. Методы получения и установки (и связанные функции) для доступа к атрибуту создаются автоматически. Например, следующая функция создана для класса счетчик, который был определен ранее.

function (this, super){
  i = 0
  [
   function inc(){ i++ },
   function getI(){ i },
   function setI(_i){ i = _i }
  ]}

Далее, класс ниже, иллюстрирует то, как выглядит класс skelton. Он имеет метод и набор полей для связи функций Pnuts. Конструктор вызывает функцию выше и инициализирует закрытые поля с каждым элементом массива, возвращаемым функцией. Он не содержит фактический код для тела метода. Вместо этого, каждый метод просто вызывает функцию Pnuts, связанную с соответствующим частным полем.

public class Counter {
 static PnutsFunction function_factory;
 static Context context;
 private PnutsFunction inc_function;
 private PnutsFunction get_function;
 private PnutsFunction set_function;
 public static void attach(PnutsFunction function_factory, Context context){
     Counter.function_factory = function_factory;
     Counter.context = context;
 }
 public Counter(){
   PnutsFunction[] function_array = function_factory.call(new Object[]{this, superProxy}, context);
   this.inc_function = function_array[0];
   this.get_function = function_array[1];
   this.set_function = function_array[2];
 }
 public Object inc(){
  this.inc_function.call(new Object[]{}, context);
 }
 ...}

Поскольку функция Pnuts имеет предварительное лексическое правило, классы, определенные в Pnuts имеют тоже предварительное правило. Если класс определяется в локальной области, символы в локальной области видны из класса определения. Например, следующая функция создает экземпляр счетчика, переменная которого используется в методе inc(), получаемая из параметра функции.

function counter (i){
 new Object() {
    inc(){ i++ }
 }}

Аналогично, атрибут класса может быть ссылкой из функций, определенных в методе.

class Counnter {
  i = 0
  inc_func(){
    {-> i++}
  }}

Легкий генератор в Pnuts 1.1

Pnuts 1.1 позаимствовал идею простого генератора у Phyton, и называется «легкий генератор». Это фундаментальные особенности языка в Pnuts, наряду со смыканием и модульной системой. Легкий генератор в Pnuts почти такой же быстрый, как и функциональный вызов. Некоторые из преимуществ перезаписи обратного вызова в стиль генератора:

  • Может иметь контроль, в отличии от обратного вызова функции
  • Может отменить итерации
  • Сценарий может быть упрощен путем замены вложенной функции циклом for.
  • Операция зачистки может быть упрощена финальным блоком в генераторе функции.

Например

function safeopen(file){
    in = open(file)
    try {
        yield in
    } finally {
        close(in)
    }
 }
 for (in: safeopen(file)){
     ....   // no need to close
 }

Несколько реализаций в Pnuts

Pnuts имеет два вида языковых реализаций в нем; Один из них - компилятор байт-кода, второй – интепретатор AST. Я думаю, что компилятор байт-кода следует использовать в большинстве случаев, но АСТ переводчик иногда необходим для любой из следующих причин.

  • Байт-код компилятор не всегда может составить сценарии успешно. Например,
 x = 1
   x++
   x++
   ...
   (3000 times)

может потерпеть неудачу из-за ограничений JVM. Pnuts использует интерпретатор ASTв качестве запасного варианта, когда компилятор байт-кода выдает ClassFormatError. - Компилятор байт-кода требует RuntimePermission ("createClassLoader"), и RuntimePermission ("getProtectionDomain"). С другой стороны, интерпретатор АСТ может работать без каких-либо разрешений. - На интерпретаторе AST, лексическая область может быть сериализована / десериализована, чего не сделать на компиляторе байт-кода.

function counter(x){
    function inc() x++
  }
  c = counter(0)
  c()  // --> 0
  c()  // --> 1
  writeObject(c, "d:/tmp/c.ser")
  ...
  c = readObject("d:/tmp/c.ser")
  c()  // --> 2

Еще одно преимущество наличия двух реализаций является то, что у нас больше шансов найти то, что в описании отсутствует. Например, семантика объявления переменной была неоднозначной, более конкретно, не было описания об объявлении переменной в условных операторах.

Управление абстракцией в Pnuts

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

Предположим, вы хотите преобразовать файл ZIP в файл LHA (или любой файл архива), без создания временных файлов.

В Java, вы должны использовать java.util.zip.ZipInputStream, чтобы читать записи ZIP:

 ZipInputStream zin = new ZipInputStream(in);
   ZipEntry entry;
   while ((entry = in.getNextEntry()) != null){
     ... }

После чего я бы использовал библиотеку LHA для Java для создания LHA файлов. Основное использование этого API:

LhaOutputStream lout = new LhaOutputStream(out);
   for (file: files){
      LhaHeader hdr = new LhaHeader(file.getName());
      lout.putNextEntry(hdr);
      lout.write(...);
      lout.closeEntry();
   }

Процедура, с помощью которого файл архива создается аналогично java.util.zip. *. Так, конвертирование ZIP в LHA может быть записана на Java так:

LhaOutputStream lout = new LhaOutputStream(out);
   ZipInputStream zin = new ZipInputStream(in);
   ZipEntry entry;
   byte[] buf = new byte[512];
   while ((entry = in.getNextEntry()) != null){
      String name = entry.getName();
      long time = entry.getTime();
      LhaHeader hdr = new LhaHeader(name);
      hdr.setLastModified(new Date(time));
      lout.putNextEntry(hdr);
      while ((nread = zin.read(buf)) != -1){
         lout.write(buf, 0, nread);
      }
      louut.closeEntry();
   }
   zin.close();
   lout.close();

В Pnuts, я бы использовал readZipEntries() и writeLhaEntries(), чтобы конвертировать ZIP файл в LHA файл.

use("lha")
   function zip2lha(infile, outfile){
     function entries(zfile){
      for (e:readZipEntries(zfile)){
        yield {"header"=>{"path"=>e.entry.name, "lastModified"=>date(e.entry.time)},
               "input"=>e.input}
      }
     }
     writeLhaEntries(outfile, entries(infile)) }

Так в чем же разница между Java кодом выше и сценарием Pnuts? Самое главное отличие в том, что контроль вызывать getNextEntry () и контроль вызывать putNextEntry () четко разделены и оба представлены как многоразовые функции; то есть readZipEntries () и writeLhaEntries ().

Invokedynamic

Как разработчик Pnuts, я должен написать о JSR292, иначе invokedynamic. Мой вывод, что, безусловно, invokeddynamic не поможет. Если бы я был членом ЕС, я бы проголосовал против, потому что не стоит добавлять новую мнемоническую спецификацию VM. Даже если возможность описывается в качестве API, он редко будет использоваться JVM языками. Pnuts является одним из самых быстрых скриптовых языков на JVM. Один секрет эффективной имплементации это сложный метод кэширования. Pnuts обычно не использует Reflection API для вызова методов, даже при доступе к свойства м JavaBeans. Pnuts генерирует метод прокси на лету и использует их. Reflection API используется только тогда, когда метод прокси не может быть использован из-за несоответствия загрузчика. Это правда, что поиск подходящих методов из фактических аргументов дорог, но влияние почти полностью невидимое, благодаря кэш-методу. В сущности, метод вызова в Pnuts даже быстрее, чем тот же метод в Ruby. Все еще стоит добавлять мнемонику? Или мнемоника для других разработчиков языков. В Pnuts можно дать подсказку для выбора конкретного метода, предполагая, что метод перегружен и два или более аргументов могут соответствовать фактическому аргументу. Когда некоторые аргументы соответствуют какому-то типу, эта информация используется чтобы выбрать подходящий способ, например String.valueOf((char[])array) . Как это может быть достигнуто с помощью invokedynamic? Кроме того, Pnuts ищет метод среди дескрипторов методов JavaBeans, но не Class.getMethods(..). И способ поиска методов/конструкторов может быть настроен. Под некоторой конфигурацией могут быть выбраны частные методы. Такая гибкость может достигаться только при application-level. Причиной замедления являются не только вызовы методов. Некоторые другие языки JVM не генерируют байт-код и не интерпретируют их в свои реализации. Даже JVM языки с компиляторами байт-кода, генерируемый код и время выполнения библиотеки должны быть эффективными. Если сгенерированный код и время выполнения библиотеки не является эффективным, то invokedynamic не будет иметь никакого эффекта.

Ссылки