Pull to refresh

Почему стоит полностью переходить на Ceylon или Kotlin (часть 1)

Reading time8 min
Views25K

В последнее время активную популярность набирает 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. Что реально меняет подход к программированию, позволяет писать гораздо более надежный, понятный и универсальный код. Но об этом в следующей части.


Tags:
Hubs:
Total votes 55: ↑40 and ↓15+25
Comments150

Articles