В последнее время активную популярность набирает Kotlin. А что если попробовать выбрать более экзотические языки, и применить к ним те же аргументы? Статья написана по мотивам этой, практически повторяя все аргументы за Kotlin. Основная задача: показать, что Ceylon может практически тоже самое, что и Kotlin, применительно к Java. Но кроме этого у Ceylon есть кое-что еще, что будет описано в следующей статье.
Хочу рассказать о новом языке программирования, который называется Ceylon, и объяснить, почему вам стоит использовать его в своём следующем проекте. Раньше я писал на Java (много и долго, более 10 лет, начиная с Java 1.4 и заканчивая Java 8), и Java мне нравилась. Затем на меня большое впечатление произвела Scala, в результате чего Java как язык стал любить несколько меньше. Но судьба свела меня с языком Сeylon, и в последние полтора года мы пишем на Ceylon везде, где только можно. В реальных коммерческих проектах, правда внутренних. И в данный момент я не представляю себе ситуации, в которой лучше было бы выбрать Java, я не рассматриваю Java как язык, на котором стоит начинать новые проекты.
Ceylon разработан в Red Hat, автор языка — Gavin King, известный по такому фреймворку как Hibernate. Он создавался людьми, которые хорошо понимают недостатки Java, основная цель заключалась в решении сугубо прикладных задач, обеспечение максимально легкой читаемости кода, избегание любых неоднозначностей и подводных камней, во главу всего стала предсказуемость и структурная красота языка. Также большое внимание уделялось приемлемому времени компиляции. В настоящее время версия языка 1.3.2, непосредственно я познакомился с языком, когда вышла версия 1.2.0.
Хотя Ceylon компилируется в JavaScript, я сконцентрируюсь на его первичной среде — JVM.
Итак, несколько причин, почему вам следует полностью переходить на Ceylon (порядок совпадает с одноименными пунктами соответствующей Kotlin статьи):
0# Совместимость с Java
Также, как и Kotlin, как и Scala, Ceylon на 100 % совместим с Java. Вы можете в буквальном смысле продолжать работать над своим старым Java-проектом, но уже используя Ceylon. Все Java-фреймворки также будут доступны, и, в каком бы фреймворке вы ни писали, Ceylon будет легко принят упрямым любителем Java. Можно без проблем вызывать из Java Ceylon код, также без проблем вызывается Java код.
1# Знакомый синтаксис
Одна из основных особенностей языка Ceylon — максимально удобно читаемый для существующих разработчиков синтаксис. Если взять существующего Java разработчика, то с пониманием Ceylon синтаксиса у него не будет ни малейших проблем. Даже такие языки, как Scala и Kotlin будут менее похожи на Java. Ниже приведен код, показывающий значительное количество конструкций языка, аналогичный примеру на Kotlin:
class Foo(String a) {
String b= "b"; // unmodifiable
variable Integer i = 0; // variable means modifiable
void hello() {
value str = "Hello";
print("``str`` World");
}
Integer sum(Integer x, Integer y) {
return x + y;
}
Float maxOf(Float a, Float b) => if (a > b) then a else b
}
Соответственно можно без проблем продолжать писать в Java стиле на Ceylon.
2# Интерполяция строк
Это как бы более умная и читабельная версия String.format() из Java, встроенная в язык:
value x = 4;
value y = 7;
print("sum of ``x`` and ``y`` is ``x + y``") ; // sum of 4 and 7 is 11
ИМХО синтаксис здесь будет поприятнее, чем в Kotlin, с Java даже не хочется сравнивать.
3# Выведение типа
Ceylon будет выводить ваши типы, если вы посчитаете, что это улучшит читабельность:
value a = "abc"; // type inferred to String
value b = 4; // type inferred to Integer
Float c = 0.7; // type declared explicitly
List<String> d = ArrayList<String>(); // type declared explicitly
4# Умные приведения типов (Smart Casts)
Компилятор Ceylon отслеживает вашу логику и по мере возможности автоматически выполняет приведение типов, т. е. вам больше не нужны проверки instanceof после явных приведений:
if (is String obj) {
print(obj.uppercased) // obj is now known to be a String
}
5# Интуитивные равенства (Intuitive Equals)
Можно больше не вызывать явно equals(), потому что оператор == теперь проверяет структурное равенство:
value john1 = Person("John"); //we override equals in Person
value john2 = Person("John");
print(john1 == john2); // true (structural equality)
print(john1 === john2); // false (referential equality)
6# Аргументы по умолчанию
Больше не нужно определять несколько одинаковых методов с разными аргументами:
void build(String title, Integer width = 800, Integer height = 600) {
return Frame(title, width, height);
}
7# Именованные аргументы
В сочетании с аргументами по умолчанию именованные аргументы избавляют от необходимости использовать Строителей:
build("PacMan", 400, 300) // equivalent
build {title = "PacMan"; width = 400; height = 300;} // equivalent
build {title = "PacMan"; height = 300;} // equivalent with default width
8# Выражение switch
Оператор ветвления заменён гораздо более читабельным и гибким в применении выражением switch:
switch (obj)
case(1) { print("x is 1"); }
case(2) { print("x is 2"); }
case(3 | 4) { print("x is 3 or 4"); }
case(is String) { print ("x is String"); }
case([Integer a, Float b, String c]) {print ("x is tuple with Integer ``a``, Float ``b`` and String ``c``");}
else { print("x is out of range");}
switch может работать как выражение, также результат switch может быть присвоен переменной:
Boolean|IllegalStateException res =
switch(obj)
case(null) false
case(is String) true
else IllegalStateException();
Это не полноценный pattern matching, но для большинства случаев хватает и текущего функционала.
В отличие от Kotlin у Ceylon требуется, чтобы к switch все условия были disjoint, то есть не пересекались, что для switch гораздо более логично. Если требуется сопоставление по диапазону или условия могут пересекаться, то нужно использовать обычный if.
9# Свойства
Можно добавить публичным полям кастомное поведение set & get, т. е. перестать набивать код безумными геттерами и сеттерами.
class Frame() {
variable Integer width = 800;
variable Integer height = 600;
Integer pixels => width * height;
}
10# Data Class
К сожалению данного функционала пока нет. Очень хотелось бы иметь иммутабельные классы, у которых автоматом переопределен toString(), equals(), hashCode() и copy(), но, в отличие от Java, не занимали 100 строк кода.
Но то, что этого пока нет в языке, не означает что это невозможно сделать. Приведу пример, как нужный функционал реализован у нас через библиотеки, средствами самого языка:
class Person(shared String name,
shared String email,
shared Integer age) extends DataObject() {}
value john = Person("John", "john@gmail.com", 112);
value johnAfterBirhstday = john.copy<Person>({`Person.age`->113;});
assertEquals(john, john.copy<Person>());
assertEquals(john.hash, john.copy<Person>().hash);
То есть на уровне библиотек получилось переопределить toString, оставить класс иммутабельным, мы получили возможность создавать клоны и изменениями отдельных аттрибутов. К сожалению работает это не так быстро, как могло быть, если бы поддержка была в языке. И нет проверки типов во время компиляции = если мы склонируем с переопределениев возраста и в качестве значения укажем строку, получим ошибку в рантайме. То, что такого функционала пока нет — безусловно плохо. Но то, что нужный функционал при необъодимости можем написать самостоятельно на уровне библиотеки — это очень хорошо.
11# Перегрузка оператора (Operator Overloading)
Заранее определённый набор операторов, которые можно перегружать для улучшения читабельности:
class Vec(shared Float x, shared Float y) satisfies Summable<Vec> {
shared actual Vec plus(Vec v) => Vec(x + v.x, y + v.y);
}
value v = Vec(2.0, 3.0) + Vec(4.0, 1.0);
12# Деструктурирующие объявления (Destructuring Declarations)
Некоторые объекты могут быть деструктурированы, что бывает полезно, к примеру, для итерирования map:
for ([key -> [val1, val2, val3]] in map) {
print("Key: ``key``");
print("Value: ``val1``, ``val2``, ``val3``");
}
13# Диапазоны (Ranges)
Для улучшения читабельности:
for (i in 1..100) { ... }
for (i in 0 : 100) { ... }
for (i in (2..10).by(2)) { ... }
for (i in 10..2) { ... }
if (x in 1..10) { ... }
В отличие от Kotlin обошлось без ключевого слова downTo.
14# Функции-расширения (Extension Functions)
Их нет. Возможно появится, в ранних спецификациях языка такая возможность рассматривалась. Но вместо функций расширений в принципе работают top level функции. Если мы, допустим, хотим добавить к классу String метод sayHello, то "world".sayHello() выглядит не намного лучше чем sayHello("world"). В будущем они могут появиться.
В принципе соответствующие функции, доступные для класса, позволяет находить сама IDE, иногда это работает.
15# Безопасность Null
Java следует называть почти статично типизированным языком. Внутри него переменная типа String не гарантированно ссылается на String — она может ссылаться на null. И хотя мы к этому привыкли, это снижает безопасность проверки на статичное типизирование, и в результате Java-разработчики вынуждены жить в постоянном страхе перед NPE.
В Ceylon эта проблема решена посредством разделения на типы, допускающие и не допускающие значение null. По умолчанию типы не допускают null, но их можно преобразовать в допускающие, если добавить ?:
variable String a = "abc";
a = null; // compile error
variable String? b = "xyz";
b = null; // no problem
За счет функционала union types String? это просто синтаксический сахар для String|Null. Соответственно можно написать:
variable String|Null с = "xyz";
с = null; // no problem
Ceylon заставляет вас бороться с NPE, когда вы обращаетесь к типу, допускающему null:
value x = b.length // compile error: b might be null
Возможно, выглядит громоздко, но благодаря нескольким своим возможностям действительно полезно. У нас всё ещё есть умные приведения типов, когда типы, допускающие null, преобразуются в не допускающие:
if (!exists b) { return; }
value x = b.length // no problem
Также можно использовать безопасный вызов ?., он возвращает значение null вместо бросания NPE:
value x = b?.length; // type of x is nullable Int
Можно объединять безопасные вызовы в цепочки, чтобы избегать вложенных проверок если-не-null, которые иногда мы пишем в других языках. А если нам по умолчанию нужно не null-значение, то воспользуемся elvis-оператором else
value name = ship?.captain?.name else "unknown";
Если всё это вам не подходит и вам совершенно точно нужны NPE, то скажите об этом явно:
value x = b?.length else NullPointerException() // same as below
assert(!NullPointerException x);
16# Улучшенные лямбды
Это хорошая система лямбд — идеальный баланс между читабельностью и лаконичностью благодаря нескольким толковым решениям. Синтаксис прост:
value sum = (Integer x, Integer y) => x + y; // type: Integer(Integer, Integer)
value res = sum(4,7) // res == 11
Соответственно синтаксис может быть:
numbers.filter( (x) => x.isPrime() );
numbers.filter(isPrime)
Это позволяет нам писать лаконичный функциональный код:
persons
.filter ( (it) => it.age >= 18)
.sort(byIncreasing(Person.name))
.map ( Person.email )
.each ( print );
Система лямбд плюс синтаксические особенности языка, делает Ceylon неплохим инструментом для создания DSL. Пример DSL, похожего на Anko, в синтаксисе Ceylon:
VerticalLayout {
padding = dip(30); {
editText {
hint = "Name";
textSize = 24.0;
},
editText {
hint = "Password";
textSize = 24.0;
},
button {
"Login";
textSize = 45.0;
} }
};
17# Поддержка IDE
Между прочим, она достаточно неплохая. Есть eclipse плагин, есть IDEA плагин. Да, по части фич и багов все несколько хуже, чем в Scala или Kotlin. Но в принципе работать можно и достаточно комфортно, проблемы IDE на скорости разработки практически не сказываются.
Итого, если брать сильные стороны Kotlin, Ceylon уступает Kotlin отсутствием функционала DataObject (который можно эмулировать самостоятельно средствами библиотек). В остальном он обеспечивает не меньшие возможности, но с более приятным для чтения синтаксисом.
Ну и так же, как и на Kotlin, на Ceylon можно писать для Android.
Прочитав вышесказанное, может сложиться впечатление — а зачем нам это? Тоже самое есть и в Kotlin, практически 1 в 1.
А то, что в Ceylon есть вещи, которых нет ни в Kotlin, ни в Scala, и за счет этих вещей сам язык во многом гораздо лучше, чем другие языки. Например union types, intersection types, enumerated types, более мощные generics, модульность, herd, аннотации и метамодель, кортежи, for comprehensions. Что реально меняет подход к программированию, позволяет писать гораздо более надежный, понятный и универсальный код. Но об этом в следующей части.
https://ceylon-lang.org/documentation/1.3/introduction/
https://ceylon-lang.org/documentation/1.3/faq/language-design/
https://dzone.com/articles/a-qa-with-gavin-king-on-ceylon
https://www.slant.co/versus/116/390/~scala_vs_ceylon
https://www.slant.co/versus/390/1543/~ceylon_vs_kotlin
https://dzone.com/articles/ceylon-enterprise-ready
http://tryge.com/2013/12/13/ceylon-and-kotlin/