Об островах

2014-11-09

Или о сортах кофе.

Java logo

Началось все в апреле. На JavaDay в Омск приехал Филипп Торчинский и рассказывал про Kotlin. Показывал, как можно применять Kotlin для веб разработки. В сентябре я снова встретился с Филиппом и он снова рассказывал про Kotlin. Но рядом упомянул волшебное слово Android. И решил я посмотреть, что за Kotlin такой. Начал читать и возникло стойкое дежавю. И тут наткнулся на новость о выходе Ceylon версии 1.1. Вот откуда дежавю.

И Котлин, и Цейлон являются языками для JVM со строгой статической типизацией. Они оба нацелены на исправление некоторых родовых недостатков Явы, которые многие программисты хотели бы видеть исправленными. За счет потери обратной совместимости с Явой. Но они оба считают себя проще, понятнее и легче для изучения, чем Scala. Хотя Скала тоже исправляет недостатки Явы, но делает это излишне академично.

Ява — это остров в Индонезии, где выращивают кофе, видимо, полюбившийся в свое время программистам Sun Microsystems. Цейлон, он же Шри-Ланка, — это остров чуть южнее Индостана, где выращивают чай. Котлин — это остров в Финском заливе, рядышком с Санкт-Петербургом, где находится город Кронштадт и где в настоящий момент ремонтируется Аврора. Видимо, петербуржцы из JetBrains решили взять название ближайшего острова.

Kotlin logo

Из Явы специально выпилили все операции с прямыми указателями на память ради безопасности, чтобы избежать сегфолтов и прочих проблем. Однако в Яве осталось значение null как почти прямой аналог нулевого указателя. И при обращении к null возникает NullPointerException. Это, конечно, не полный крах приложения, нульпоинтер можно отловить и что-то с этим сделать. Тем не менее, этот эксепшен — самая частая проблема кода на Яве.

И Котлин, и Цейлон борются с нульпоинтерами, добавив проверку на null в систему типов. Если у вас есть переменная типа String, она не может принимать значение null. Это гарантируется компилятором, и подобная попытка вызовет ошибку компиляции. Если же переменной нужно присвоить null, придется объявить её как String? (с вопросиком). Это совсем другой тип переменной, и преобразовать String? в String можно только после явной проверки на null.

var a : String = "abc"
a = null // compilation error

var b : String? = "abc"
b = null // ok

val l = b?.length() ?: -1
variable String a = "abc";
a = null; // compilation error

variable String? b = "abc";
b = null; // ok

value l2 = b?.size else -1;

Подобные проверки возможны и в Яве, с помощью нестандартных аннотаций вроде @Nullable, @NotNull и т.п. Однако в этом случае проверки выполняются IDE или статическими анализаторами, а не компилятором.

В Цейлоне тип String? на самом деле является сокращением для юнион типа String|Null. Юнион типы (например String|Integer) являются важным свойством Цейлона и лежат в основе многих фич языка. А Null — это специальный тип, у которого есть единственное валидное значение null. Соответственно, переменная типа String|Null может содержать либо строку (ни в коем случае не null), либо само значение null.

Котлин и Цейлон, вслед за Скалой, уделяют много внимания иммутабельности. Все переменные и поля классов объявляются как изменяемые или неизменные. В последнем случае им можно присвоить значение лишь раз, аналогично final в Яве. Все коллекции по умолчанию неизменяемые. А изменяемые коллекции — это совсем другие классы.

val s1 = "abc"
s1 = "def" //compilation error

var s2 = "abc"
s2 = "def"

val l = listOf(1, 2, 3)
val m = arrayListOf<Int>()
m.addAll(l)
value s1 = "abc";
s1 = "def"; //compilation error

variable value s2 = "abc";
s2 = "def";

value l = [1, 2, 3];
value m = ArrayList();
m.addAll(l);

Котлин и Цейлон, вслед за C#, добавляют проперти и атрибуты в классы, избавляя от головной боли геттеров и сеттеров. Отныне у классов вообще нет каких-либо полей, непосредственно содержащих ссылку на значение. Отныне любой доступ, который выглядит как обращение к полю, осуществляется через геттер или сеттер. А для неизменяемых "полей" и сеттера нет.

class Address(var street : String,
              var building : String) {
    public var asString : String
    get() {
        return "$street, $building"
    }
    set(value) {
        //parse value
    }
}
class Address(street, building) {
    shared variable String street;
    shared variable String building;
    shared String asString =>
        "``street``, ``building``";
    assign asString {
        //parse value
    }
}

В генериках и Котлин, и Цейлон, вслед на Скалой, радуют штукой под названием declaration-site variance. Variance — это отношение наследования генерик-типов к направлению наследования их аргументов-типов. Например, List<Number> и List<Integer> не являются наследниками друг друга, это называется инвариантностью. Но Integer является подклассом Number, а значит, вполне безопасно извлекать элементы из List<Integer> и помещать их в List<Number>. И тогда List<Number> будет суперклассом List<Integer>. В Яве можно записать List<? extends Number>, т.е. список подклассов Number. Это называется ковариантностью, и справедливо только если мы извлекаем элементы из списка. В то же время вполне нормально помещать Integer в List<Number>. Тогда уже List<Number> будет подклассом List<Integer>. Это называется контрвариантностью, и справедливо только если мы помещаем элементы в список.

Если вы ничего не поняли в предыдущем абзаце — это нормально. Я всегда так себя чувствую, когда погружаюсь в дебри генериков. Я даже думаю, что динамическая типизация придумана не зря, ибо избавляет от подобной боли. Ну а declaration-site variance в Котлине и Цейлоне лишь позволяет вместо ломания головы по поводу variance просто сказать, возвращает ли наш генерик объекты этого типа или же принимает их. Вы просто указываете in или out, а компилятор уже сам разберется какие типы куда можно безопасно приводить.

class Box<out T>(val item : T) {
    public fun get() : T {
        return item;
    }
}

val intBox = Box<Int>(123);
val numBox : Box<Number> = intBox;
val num : Number = numBox.get();
class Box<out Item>(Item item) {
    shared Item get() {
        return item;
    }
}

Box<Integer> intBox = Box(123);
Box<Number<Integer>> numBox = intBox;
Number<Integer> num = numBox.get();

В Котлине и Цейлоне есть множество других приятностей. Во многих случаях можно не указывать типы значений, компилятор сам догадается. В интерфейсах (трейтах) можно описывать реализации методов, что позволяет вместо делегирования, как в Яве, устроить просто множественное наследование от нужных интерфейсов. Можно перегружать операторы. В Котлине это сделано по-питоновски просто: оператору соответствует определенный метод объекта. В Цейлоне для каждого оператора нагородили интерфейсов, которые нужно реализовать, чтобы оператор заработал с вашими объектами. Есть первоклассные и высокопорядочные функции, лямбдочки, замыкания и прочие функциональные прелести. Есть возможность писать в DSL стиле.

fun <T, R> List<T>.map(transform : (T) -> R) : List<R> {
    val result = arrayListOf<R>()
    for (item in this)
        result.add(transform(item))
    return result
}

listOf(1, 2, 3).map { it -> it * 2 }
List<Result> map<Item, Result>
        (List<Item> list)(Result(Item) transform) {
    List<Result> result = ArrayList();
    for (item in list) {
        result.add(transform(item));
    }
    return result;
}

map<Integer, Integer>(Array{1, 2, 3})
    ((item) => item * 2);

Ну и оба они "компилируются" в Ява-скрипт. Хотя и там и там рантайм под Ява-скриптом сильно отличается (по использованию) от рантайма под JVM. Полной прозрачности не наблюдается.

Компилятор Котлина, как и Скалы, производит байткод, т.е. .class файлы. Дальше с этими файлами можно делать что угодно. Лишь нужна небольшая стандартная рантайм библиотечка, чтобы этот байткод корректно заработал. В результате и Котлин, и Скала легко интегрируются в существующую инфраструктуру Явы. И, например, на них без особых проблем можно писать под Андроид.

А вот компилятор Цейлона согласен работать только с модулями, которые состоят из пакетов, которые содержат классы, функции и прочие элементы языка. В результате получается .car файл — архив модуля, внутри которого прячется все тот же JVMовый байткод. Каждый модуль имеет имя, версию и зависимости. Ребята переизобрели Maven и фактически реализовали Jigsaw. Оно хорошо для редхатового же JBoss, но вряд ли удобно где-то еще.

Ceylon logo

Как вы успели заметить, Котлин указывает типы после имени переменной, через двоеточие, как в Паскале и Скале. А вот Цейлон остался верным сишному (и Явовому) стилю. Вот только, похоже, для автоматического угадывания типов, точнее для опускания типов в коде, паскалевская нотация удобнее. В Цейлоне, например, приходится добавлять глупое ключевое слово value. Еще Цейлон грешен многочисленными длинными аннотациями, а также чудовищно навороченными иерархиями стандартных интерфейсов.

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

Scala logo

Будущее Цейлона не ясно. Вероятно, он будет иметь хождение внутри инфрастуктуры РедХата. Но не понятно, можно ли его использовать где-то еще. Под Андроид, вроде, он не очень применим.

У Котлина будущее посветлее. Весной его продвигали под веб разработку. Осенью его стали продвигать под Андроид разработку. Хоть разработчики и честно сознаются, что если вы уже используете Скалу, то Котлин вам не нужен, но пара технических преимуществ перед Скалой у Котлина есть: он быстрее компилируется (скорость компиляции была одной из целей разработчиков), его рантайм меньше скалового (что важно для мобильной разработки).

Более подробно изыскания про Котлин и Скалу я изложил на ИТ-субботнике.