Об островах
2014-11-09
Или о сортах кофе.
Началось все в апреле. На JavaDay в Омск приехал Филипп Торчинский и рассказывал про Kotlin. Показывал, как можно применять Kotlin для веб разработки. В сентябре я снова встретился с Филиппом и он снова рассказывал про Kotlin. Но рядом упомянул волшебное слово Android. И решил я посмотреть, что за Kotlin такой. Начал читать и возникло стойкое дежавю. И тут наткнулся на новость о выходе Ceylon версии 1.1. Вот откуда дежавю.
И Котлин, и Цейлон являются языками для JVM со строгой статической типизацией. Они оба нацелены на исправление некоторых родовых недостатков Явы, которые многие программисты хотели бы видеть исправленными. За счет потери обратной совместимости с Явой. Но они оба считают себя проще, понятнее и легче для изучения, чем Scala. Хотя Скала тоже исправляет недостатки Явы, но делает это излишне академично.
Ява — это остров в Индонезии, где выращивают кофе, видимо, полюбившийся в свое время программистам Sun Microsystems. Цейлон, он же Шри-Ланка, — это остров чуть южнее Индостана, где выращивают чай. Котлин — это остров в Финском заливе, рядышком с Санкт-Петербургом, где находится город Кронштадт и где в настоящий момент ремонтируется Аврора. Видимо, петербуржцы из JetBrains решили взять название ближайшего острова.
Из Явы специально выпилили все операции с прямыми указателями на память ради безопасности,
чтобы избежать сегфолтов и прочих проблем.
Однако в Яве осталось значение 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,
но вряд ли удобно где-то еще.
Как вы успели заметить, Котлин указывает типы после имени переменной, через двоеточие,
как в Паскале и Скале.
А вот Цейлон остался верным сишному (и Явовому) стилю.
Вот только, похоже, для автоматического угадывания типов, точнее для опускания типов в коде,
паскалевская нотация удобнее.
В Цейлоне, например, приходится добавлять глупое ключевое слово value
.
Еще Цейлон грешен многочисленными длинными аннотациями,
а также чудовищно навороченными иерархиями стандартных интерфейсов.
Это я к чему. Если мы говорим, что наш язык такой же выразительный, как, скажем, Питон, но проще и понятнее, чем Скала, то давайте действительно делать его выразительным, простым и понятным.
Будущее Цейлона не ясно. Вероятно, он будет иметь хождение внутри инфрастуктуры РедХата. Но не понятно, можно ли его использовать где-то еще. Под Андроид, вроде, он не очень применим.
У Котлина будущее посветлее. Весной его продвигали под веб разработку. Осенью его стали продвигать под Андроид разработку. Хоть разработчики и честно сознаются, что если вы уже используете Скалу, то Котлин вам не нужен, но пара технических преимуществ перед Скалой у Котлина есть: он быстрее компилируется (скорость компиляции была одной из целей разработчиков), его рантайм меньше скалового (что важно для мобильной разработки).
Более подробно изыскания про Котлин и Скалу я изложил на ИТ-субботнике.