суббота, 24 апреля 2010 г.

[prog.flame] Попрограммировал на Java, делюсь впечатлениями. Часть III. Какую бы Java хотелось иметь (максимальный вариант).

Продолжение заметок о впечатлениях после программирования на Java. Предыдущие части здесь:

Часть I
Часть II

В этот раз я расскажу о том, что хотелось бы иметь в языке Java, если бы его более-менее серьезно переработать (как язык, так и саму JVM).

Свой рассказ я разделил на две части. В первой части описываются то, что имеет более-менее осязаемые очертания. Тогда как во второй части речь пойдет о том, что хотелось бы видеть, но пока не понятно, как именно это должно выглядеть.

Итак, сначала о том, что я себе хорошо представляю.

1. Мне пришлось в своем проекте на Java перепаковывать биты в байты и обратно – после C++ без unsigned char-ов и unsigned int-ов было непривычно. Приходилось делать лишнюю работу, мелкую и нудную. Поэтому очень хотелось бы видеть в Java хотя бы unsigned byte. Чтобы можно было писать так:

class ByteTest {
   static public void main(String[] args) {
      ubyte v = 0x83;
      ubyte b = v >> 1;
   }
}

Насколько я понимаю, беззнаковые целые не поддерживаются на уровне JVM. Зря они так.

2. Очень бы не помешали туплы для того, чтобы из методов можно было возвращать несколько значений. Этого и в C++ нынешнем нет, но в C++ хотя бы есть возможность передавать аргументы по неконстантным ссылкам/указателям. Поэтому в C++ возврат нескольких int-ов из функции/метода гораздо проще, чем в Java. Так что, имхо, в Java туплы даже нужнее, чем в C++.

Очевидно, что туплы – это штука неоднозначная (как и автоматический вывод типов). Сегодня одному разработчику будет удобно возвращать из метода тупл. А через год другой разработчик будет гадать – такой же смысл имеет третий элемент возвращенного тупла. Поэтому использование туплов можно было бы ограничить: например, запретить их возврат из public-методов классов. Тогда бы и разработка на Java упростилась бы, и сопровождаемость кода не пострадала бы. Лично мне было бы приятнее писать так:

class Demo {
   public void doSomething() {
      int min;
      int max;
      (min, max) = findMinAndMax();
      ...
   }

   private (int, int) findMinAndMax() {
      int min = someInternalData[0];
      int max = someInternalData[0];
      for( int item : someInternalData ) {
         if( item < min ) min = item;
         if( item > max ) max = item;
      }
      return (min, max);
   }

   ...
}

а не так, как сейчас:

class Demo {
   public void doSomething() {
      MinAndMax minMax = findMinAndMax();
      ...
   }

   private (int, int) findMinAndMax() {
      MinAndMax result = new MinAndMax( someInternalData[0], someInternalData[0] );

      for( int item : someInternalData ) {
         if( item < result.min ) result.min = item;
         if( item > result.max ) result.max = item;
      }
      return result;
   }

   ...
}

private class MinAndMax {
   int min;
   int max;

   MinAndMax(int min, int max) {
      this.min = min;
      this.max = max;
   }
}

3. В Java очень сильно не хватает константности объектов. В C++ я к этому привык. Бывало, что компилятор бил разработчика по рукам, предупреждая ошибки. Да и в связи с hype вокруг функционального программирования и многоядерных процессоров, языки без константности для объектов уже выглядят опасными.

За основу я бы взял вариант из языка D, но без хвостовой константности. Разделил бы все ссылки на три типа: обычные, readonly и immutable. Если есть обычная ссылка на объект, то у объекта можно вызывать любые методы. Если есть readonly-ссылка или immutable-ссылка, то можно вызывать только immutable-методы.

Соответственно, объекты в программе делятся на обычные (изменяемые) и immutable (изменить которые нельзя). Обычные объекты можно передавать по обычным или readonly ссылкам, но нельзя передавать по immutable-ссылке.

Смысл у readonly-ссылок такой же, как у const-ссылок и указателей в C++: мы не можем модифицировать объект, но эта модификация может быть разрешена для кого-то другого. Например:

class UserInfo {
   private String name;

   UserInfo(String name) { this.name = name; }

   immutable public String name() { return this.name; }
   public String setName(String name) { this.name = name; }
}

class UserObserver {
   readonly private UserInfo user;

   UserObserver(readonly UserInfo user) { this.user = user; }

   public void print() { System.out.println(user.name()); }
}

class Demo {
   static public void main(String[] args) {
      UserInfo ui = new UserInto("John Smith");
      UserObserver observer = new UserObserver(ui);

      observer.print(); // => John Smith

      ui.setName("John Woo");

      observer.print(); // => John Woo
   }
}

А вот смысл immutable в том, что компилятор гарантирует, что объект никогда не изменится. Если есть immutable ссылка на объект A, то компилятор разрешает вызывать только те методы объекта A, которые помечены как immutable. И компилятор следит за тем, чтобы внутри immutable-методов состояние объекта не изменялось.

Представим, что в примере выше в классе UserObserver хранилась бы не readonly, а immutable ссылка на UserInfo. В этом случае UserObserver был бы защищен от “внезапных” модификаций объекта UserInfo где-то в других местах программы.

Отсутствие хвостовой константности (т.е. когда все ссылки внутри immutable объекта автоматически становятся immutable ссылками) позволяет легко организовывать обмен сообщениями между нитями в программе. Например, пусть есть сообщение ChangeLogStreamNotify, которое рассылается сразу нескольким нитям:

class ChangeLogStreamNotify { 
   public LogStream stream;

   ChangeLogStreamNotify(LogStream stream) { this.stream = stream; }
}

Сам объект сообщения поступает подписчикам в виде immutable объекта. Поэтому подписчики не смогут заменить в нем значение stream. Т.е. при доступе к экземпляру ChangeLogStreamNotify из разных потоков не нужно никакой синхронизации.

Однако, ссылка на LogStream внутри ChangeLogStreamNotify является обычной ссылкой. Поэтому для объекта LogStream можно вызывать любые методы:

class WorkThread {
   public void onChangeLogStreamNotify(
         immutable ChangeLogStreamNotify notify) {
      notify.stream.debug("log stream changed");
   }
}

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

1. В Java не хватает хоть какого-то подобия нормального множественного наследования или же каких-то механизмов подмешивания общего кода в разные классы. Что-то типа механизма mixin-ов из Ruby или trait-ов из Scala. Хотя в Scala, имхо, trait-ы переусложнили. Нужно бы чего-нибудь попроще.

Например, был у меня интерфейс TlvCompoundItem с одним методом tlvFields.

public interface TlvCompoundItem extends TlvItem {
    TlvFieldInfo[] tlvFields();
}

Этот интерфейс реализует базовый класс для PDU (элементов прикладного протокола):

abstract public class Pdu implements TlvCompoundItem {
    @Override
    abstract public TlvFieldInfo[] tlvFields();
    ...
}

И затем каждый наследник Pdu должен определять у себя tlvFields одним и тем же способом:

public class SomeConcretePdu extends Pdu {
    ...
    @Override
    public TlvFieldInfo[] tlvFields() {
        return tlvFieldsDecription;
    }

    static private TlvFieldInfo[] tlvFieldsDescription = ...;
}

Т.е. в каждом наследнике Pdu есть статический атрибут с описанием TLV-полей, и всегда он имеет имя tlvFieldsDescription. И реализация метода tlvFields всегда возвращает этот атрибут.

Вот это дублирование одинакового кода tlvFields и не нравится. Можно было бы поступить как-то так (это только набросок):

mixin PduFieldsGetter requires(TlvFieldInfo[] fields) {
   public TlvFieldInfo[] tlvFields() {
      return fields;
   }
}

public class SomeConcretePdu
   extends Pdu
   uses PduFieldsGetter(tlvFieldsDescription)
{
   ... // Здесь уже не нужно объявлять tlvFields().

   static private TlvFieldInfo[] tlvFieldsDescription = ...;
}

2. Хотелось бы иметь в Java ссылки, которые гарантированно не могут быть нулевыми. Т.е., чтобы Java предлагала бы решение проблемы нулевых ссылок, как это сейчас делает Eiffel.

Только вот как это делать – это вопрос. Либо же в язык вводится понятие ненулевой ссылки (например, String! вместо String) и компилятор будет отслеживать действия с этой ссылкой (так попытались сделать в языке Nice). Либо же нужно добавлять в язык что-то вроде типа Option (с сопутствующими ему вариантами None и Some) и паттерн-матчинга, чтобы нужно было явно разделять ситуации с нулевыми и ненулевыми ссылками.

Я бы предпочел вариант без паттерн-матчинга и типа Option. Запись String! более привычна для императивного программиста. Да и гигатонны уже написанного Java кода в таком случае было бы гораздо проще переиспользовать.


Вот такие у меня мысли о том, как сделать из Java нормальный ;) язык. Если собрать эти предложения и то, о чем я писал раньше, то получится неплохой, прагматичный язык. На который я бы лично перешел бы с C++. Наверное :)

12 комментариев:

  1. А так ли необходимы различные const модификаторы?
    Не приведет ли их введение к необоснованному усложнению языка?
    Как часто в твоей практике сишный конст не мог решить проблему?

    По поводу нулевых значений.
    У меня практически отсутствует опыт работы с явой поэтому интересно -- нулевые ссылки, это реальная проблема, для которой стоит городить костыли по типу Eiffel?
    Например в sql с null вполне удобно работать и в с++ мне его не хватает.

    такие вот вопросы :)

    ОтветитьУдалить
  2. >А так ли необходимы различные const модификаторы?

    Да. Механизм const в C++ очень хорошая штука. Это часть того, что меня в C++ до сих пор удерживает. Но этот механизм половинчатый. Он дает только readonly view (не знаю, как это лучше по русски сказать). Дополнить бы его и настоящей иммутабельностью -- вот это было бы здорово.

    >Не приведет ли их введение к необоснованному усложнению языка?

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

    >Как часто в твоей практике сишный конст не мог решить проблему?

    Да есть целый комплекс проблем (причем в языках с GC, как в Java). Например, в конструктор какого-нибудь объекта A передается ссылка на второй объект B. Объект A должен сохранить у себя B. Как он может это сделать? Выполнить dup/clone чтобы получить автономную копию? Или же можно просто сохранить ссылку, зная, что объект не изменится. Сишный конст ничего на эту тему не говорит. А вот immutable был бы в тему.

    >нулевые ссылки, это реальная проблема, для которой стоит городить костыли по типу Eiffel?

    Ну в прямых руках не страшно :) Но даже я их часто забываю выпрямить. Так что лишние гарантии со стороны компилятора не помешают. В Java null-ссылки встречаются даже чаще, чем в C++ нулевые указатели. Поскольку в C++ есть ссылки, которые просто так нулевыми не сделать. А в Java все на ссылках. Поэтому там такой механизм даже важнее, чем в плюсах.

    ОтветитьУдалить
  3. Евгений Охотников>Например, в конструктор какого-нибудь объекта A передается ссылка на второй объект B. Объект A должен сохранить у себя B. Как он может это сделать? Выполнить dup/clone чтобы получить автономную копию? Или же можно просто сохранить ссылку, зная, что объект не изменится. Сишный конст ничего на эту тему не говорит. А вот immutable был бы в тему.

    а если immutable сделать шаблонным классом, это не решит проблему?

    ОтветитьУдалить
  4. >а если immutable сделать шаблонным классом, это не решит проблему?

    В С++ или в Java?

    ОтветитьУдалить
  5. Имхо, к C++ варианты с readonly и immutable ссылками не применимы. Поскольку в C++ нет сборки мусора. Поэтому в С++ как такового immutable объекта быть не может -- кто-то за ним должен следить. (Я сейчас не про const-объекты, которые компилятор вообще может в ROM определить).

    Так что все мои слова нужно относить только к языкам с GC (вроде Java, C#, D и т.д.).

    ОтветитьУдалить
  6. Про множественное наследование слышал, а что такое
    mixin-ов из Ruby или trait-ов из Scala?
    Эти понятия (mixin и trait) как-нибудь переводятся на русский? Где можно посмотреть примеры их использования?

    ОтветитьУдалить
  7. 2Quaker:

    Про Ruby-овые mixin-ы можно прочитать в моей статье о Ruby на RSDN (http://www.rsdn.ru/article/ruby/ruby_edges.xml#ELZAE).

    Про Scala-вские trait-ы я в первый раз читал так же на RSDN: http://rsdn.ru/article/philosophy/Scala.xml#ENQAE
    Но, возможно, это описание уже устарело. Новое нужно искать где-нибудь на scala-lang.org (например, http://www.scala-lang.org/node/126)

    С траитами в Scala, AFAIK, есть очень тонкий момент. Называется он, кажется, линеаризация. Это когда все, что класс к себе подмешал, выстраивается в линейный список и определяется кто кого скрывает. Вот эта штука мне показалась сложной и неоднозначной. Сейчас уже не помню, но вроде бы от порядка следования mixin-ов в описании класса само поведение класса могло изменяться.

    ОтветитьУдалить
  8. Ты или например я и другие плюсисты на такой язык да перешли бы, но есть подозрения что многое явисты с него сбежали бы :)

    ОтветитьУдалить
  9. 2Rustam: насколько я знаю, многие java-исты сбегают на тот язык, за программирование на котором платят :) Если будут платить за такой, то перейдут и на него :))

    ОтветитьУдалить
  10. Про immutable vs mutable/readonly всё не так просто.

    Если immutable объект конструируется строго извне - то мы можем гарантировать его чистоту.
    А если у него есть конструктор?
    В рамках конструктора объект может (и, наверно, должен) изменять себя - дописывать значения членов-данных, например. Иначе смысл конструктора теряется.
    Но в этот отрезок времени объект mutable. И значит, он может заначить ссылку на самого себя.

    Тут-то мы и отхватим проблем с алиасингом (двумя разнотипными ссылками на один объект).

    ОтветитьУдалить
  11. >В рамках конструктора объект может (и, наверно, должен) изменять себя - дописывать значения членов-данных, например. Иначе смысл конструктора теряется.

    Думаю, что в конструкторе immutable объекта можно разрешать только списки инициализаторов. Если объекту этого недостаточно, то на помощь придет идиома PImpl, где содержимое объекта будет конструироваться вспомогательной функцией-фабрикой.

    ОтветитьУдалить