В последнее время активную популярность набирает 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/
