Продолжаем рассказ о языке цейлон. В первой части статьи Сeylon выступал как гость на поле Kotlin. То есть брались сильные стороны и пытались их сравнить с Ceylon.
В этой части Ceylon выступит как хозяин, и перечислим те вещи, которые близки к уникальным и которые являются сильными сторонами Ceylon.
Поехали:
18# Типы — объединения (union types)
В большинстве языков программирования функция может может возвратить значения строго одного типа.
function f() {
value rnd = DefaultRandom().nextInteger(5);
return
switch(rnd)
case (0) false
case (1) 1.0
case (2) "2"
case (3) null
case (4) empty
else ComplexNumber(1.0, 2.0);
}
value v = f();Какой реальный тип будет у v? В Kotlin или Scala тип будет Object? или просто Object, как наиболее общий из возможных вариантов.
В случае Ceylon тип будет Boolean|Float|String|Null|[]|ComplexNumber.
Исходя из этого знания, если мы, допустим, попробуем выполнить код
if (is Integer v) { ...} //Не скомпилируется, v не может физически быть такого типа
if (is Float v) { ...} //Все нормально
Что это дает на практике?
Во первых, вспомним про checked исключения в Java. В Kotlin, Scala и других языках по ряду причин от них отказались. Однако потребность декларировать, что функция или метод может вернуть какое то ошибочное значение, никуда не делась. Как и никуда не делась потребность обязать пользователя как то обработать ошибочную ситуацию.
Соответственно можно, например, написать:
Integer|Exception p = Integer.parse("56");И далее пользователь обязан как то обработать ситуацию, когда вместо p вернется исключение.
Можно выполнить код, и прокинуть далее исключение:
assert(is Integer p);Или мы можем обработать все возможные варианты через switch:
switch(p)
case(is Integer) {print(p);}
else {print("ERROR");}Все это позволяет писать достаточно лаконичный и надежный код, в котором множество потенциальных ошибок проверяется на этапе компиляции. Исходя из опыта использования, union types — это реально очень удобно, очень полезно, и это то, чего очень не хватает в других языках.
Также за счет использования union types в принципе можно жить без функционала перегрузки функций. В Ceylon это убрали с целью упрощения языка, за счет чего удалось добиться весьма чистых и простых лямбд.
19# Типы — пересечения (Intersection types)
Рассмотрим код:
interface CanRun {
shared void run() => print("I am running");
}
interface CanSwim {
shared void swim() => print("I am swimming");
}
interface CanFly {
shared void fly() => print("I am flying");
}
class Duck() satisfies CanRun & CanSwim & CanFly {}
class Pigeon() satisfies CanRun & CanFly {}
class Chicken() satisfies CanRun {}
class Fish() satisfies CanSwim {}
void f(CanFly & CanSwim arg) {
arg.fly();
arg.swim();
}
f(Duck()); //OK Duck can swim and fly
f(Fish());//ERROR = fish can swim onlyМы объявили функцию, принимающую в качестве аргумента объект, который должен одновременно уметь и летать и плавать.
И мы можем без проблем вызывать соответствующие методы. Если мы передадим в эту функцию объект класса Duck — все хорошо, так как утка может и летать и плавать.
Если же мы передадим объект класса Fish — у нас ошибка компиляции, ибо рыба может только плавать, но не летать.
Используя эту особенность, мы можем ловить множество ошибок в момент компиляции, при этом мы можем пользоваться многими полезными техниками из динамических языков.
20# Типы — перечисления (enumerated types)
Можно создать абстрактный класс, и у которого могут быть строго конкретные наследники. Например:
abstract class Point()
of Polar | Cartesian {
// ...
}В результате можно писать обработчики в switch не указывая else
void printPoint(Point point) {
switch (point)
case (is Polar) {
print("r = " + point.radius.string);
print("theta = " + point.angle.string);
}
case (is Cartesian) {
print("x = " + point.x.string);
print("y = " + point.y.string);
}
}В случае, если в дальнейшем при развитии продукта мы добавим еще один подтип, компилятор найдет за нас те места, где мы пропустили явную обработку добавленного типа. В результате мы сразу поймаем ошибки в момент компиляции и их подсветит IDE.
С помощью данного функционала в Ceylon делается аналог enum в Java:
shared abstract class Animal(shared String name) of fish | duck | cat {}
shared object fish extends Animal("fish") {}
shared object duck extends Animal("duck") {}
shared object cat extends Animal("cat") {}В результате мы получаем функционал enum практически не прибегая к дополнительным абстракциям.
Если нужен функционал, аналогичный valueOf в Java, мы можем написать:
shared Animal fromStrToAnimal(String name) {
Animal? res = `Animal`.caseValues.find((el) => el.name == name);
assert(exists res);
return res;
}Соответствующие варианты enum можно использовать в switch и т.д, что во многих случаях помогает находить потенциальные ошибки в момент компиляции.
23# Алиасы типов (Type aliases)
Ceylon является языков с весьма строгой типизацией. Но иногда тип может быть довольно громоздким и запутанным. Для улучшения читаемости можно использовать алиасы типов: например можно сделать алиас интерфейса, в результате чего избавиться от необходимости указания типа дженерика:
interface People => Set<Person>;Для типов объединений или пересечений можно использовать более короткое наименование:
alias Num => Float|Integer;Или даже:
alias ListOrMap<Element> => List<Element>|Map<Integer,Element>;Можно сделать алиасы на интерфейс:
interface Strings => Collection<String>;Или на класс, причем класс с конструктором:
class People({Person*} people) => ArrayList<Person>(people);
Также планируется алиас класса на кортеж.
За счет алиасов можно во многих местах не плодить дополнительные классы или интерфейсы и добиться большей читаемости кода.
21# Кортежи
В цейлоне очень хорошая поддержка кортежей, они органично встроены в язык. В Kotlin посчитали, что они не нужны. В Scala они сделаны с ограничениями по размеру. В Ceylon кортежи представляют собой связанный список, и соответственно могут быть произвольного размера. Хотя в реальности использование кортежей из множества разнотипных элементов это весьма спорная практика, достаточно длинные кортежи могут понадобиться, например, при работе со строками таблиц баз данных.
Рассмотрим пример:
value t = ["Str", 1, 2.3];
Тип будет довольно читаемым — [String, Integer, Float]
А теперь самое вкусное — деструктуризация. Если мы получили кортеж, то можно легко получить конкретные значения. Синтаксис по удобству будет практически как в Python:
value [str, intVar, floatType] = t;
value [first, *rest] = t;
value [i, f] = rest;Деструктуризацию можно проводить внутри лямбд, внутри switch, внутри for — выглядит это достаточно читаемо. На практике за счет функционала кортежей и деструктуризации во многих случаях можно отказаться от функционала классов практически без ущерба читаемости и типобезопасности кода, это позволяет очень быстро писать прототипы как в динамических языках, но совершать меньше ошибок.
22# Конструирование коллекций (for comprehensions)
Очень полезная особенность, от которой сложно отказаться после того, как ее освоил. Попробуем проитерировать от 1 до 25 с шагом 2, исключая элементы делящиеся без остатка на 3 и возведем их в квадрат.
Рассмотрим код на python:
res = [x**2 for x in xrange(1, 25, 2) if x % 3 != 0]На Ceylon можно писать в подобном стиле:
value res = [for (x in (1:25).by(2)) if ( (x % 3) != 0) x*x];
Можно тоже самое сделать лениво:
value res = {for (x in (1:25).by(2)) if ( (x % 3) != 0) x*x};
Синтаксис работает в том числе с коллекциями:
value m = HashMap { for (i in 1..10) i -> i + 1 };К сожалению пока нет возможности так элегантно конструировать Java коллекции. Пока из коробки синтаксис будет выглядеть как:
value javaList = Arrays.asList<Integer>(*ObjectArray<Integer>.with { for (i in 1..10) i});Но написать функции, которые конструируют Java коллекции можно самостоятельно очень быстро. Синтаксис в этом случае будет как:
value javaConcurrentHashMap = createConcurrentHashMap<Integer, Integer> {for (i in 1..10) i -> i + 1};22# Модульность и Ceylon Herd
Задолго до выхода Java 9 в Ceylon существовала модульность.
module myModule "3.5.0" {
shared import ceylon.collection "1.3.2";
import ceylon.json "1.3.2";
}Система модулей уже интегрирована с maven, соответственно зависимости можно импортировать традиционными средствами. Но вообще, для Ceylon рекомендуется использовать не Maven артефакторий, а Ceylon Herd. Это отдельный сервер (который можно развернуть и локально), который хранит артефакты. В отличие от Maven, здесь можно сразу хранить документацию, а также Herd проверяет все зависимости модулей.
Если все делать правильно, получается уйти от jar hell, весьма распространенный в Java проектах.
По умолчанию модули иерархичны, каждый модуль загружается через иерархию Class Loaders. В результате мы получаем защиту, что один класс будет по одному и тому же пути в ClassPath. Можно включить поведение, как в Java, когда classpath плоский — это бывает нужно когда мы используем Java библиотеки для сериализации. Ибо при десериализации ClassLoader библиотеки не сможет загрузить класс, в который мы десериализуем, так как модуль библиотеки сериализации не содержит зависимостей на модуль, в котором определен класс, в который мы десериализуем.
24# Улучшенные дженерики
В Ceylon нет Erasure. Соответственно можно написать:
switch(obj)
case (is List<String>) {print("this is list of string)};
case (is List<Integer>) {print("this is list of Integer)};Можно для конкретного метода узнать в рантайме тип:
shared void myFunc<T>() given T satisfies Object {
Type<T> tclass = `T`;
//some actions with tClassЕсть поддержка self types. Предположим, мы хотим сделать интерфейс Comparable, который умеет сравнивать элемент как с собой, так и себя с другим элементом. Попытаемся ограничить типы традиционными средствами:
shared interface Comparable<Other>
given Other satisfies Comparable<Other> {
shared formal Integer compareTo(Other other);
shared Integer reverseCompareTo(Other other) {
return other.compareTo(this); //error: this not assignable to Other
}
}Не получилось! В одну сторону compareTo работает без проблем. А в другую не получается!
А теперь применим функционал self types:
shared interface Comparable<Other> of Other
given Other satisfies Comparable<Other> {
shared formal Integer compareTo(Other other);
shared Integer reverseCompareTo(Other other) {
return other.compareTo(this);
}
}Все компилируется, мы можем сравнивать объекты строго одного типа, работает!
Также для дженериков гораздо более компактный синтаксис, поддержка ковариантности и контравариантности, есть типы по умолчанию:
shared interface Iterable<out Element, out Absent=Null> ...В результате снова имеем гораздо лучшую и строгую типизацию по сравнению с Java. Правда некоторыми особенностями в узких местах программы пользоваться не рекомендуется, получение информации о типах в рантайме не бесплатно. Но в некритичных по скорости местах это может быть очень полезно.
24# Метамодель
В Ceylon мы в рантайме можем очень детально проинспектировать весьма многие элементы программы. Мы можем проинспектировать поля класса, мы можем проинспектировать атрибут, конкретный пакет, модуль, конкретный обобщенный тип и тому подобное.
Рассмотрим некоторые варианты:
ClassWithInitializerDeclaration v = `class Singleton`;
InterfaceDeclaration v =`interface List`;
FunctionDeclaration v =`function Iterable.map`;
FunctionDeclaration v =`function sum`;
AliasDeclaration v =`alias Number`;
ValueDeclaration v =`value Iterable.size`;
Module v =`module ceylon.language`;
Package v =`package ceylon.language.meta`;
Class<Singleton<String>,[String]> v =`Singleton<String>`;
Interface<List<Float|Integer>> v =`List<Float|Integer>`;
Interface<{Object+}> v =`{Object+}`;
Method<{Anything*},{String*},[String(Anything)]> v =`{Anything*}.map<String>`;
Function<Float,[{Float+}]> v =`sum<Float>`;
Attribute<{String*},Integer,Nothing> v =`{String*}.size`;
Class<[Float, Float, String],[Float, [Float, String]]> v =`[Float,Float,String]`;
UnionType<Float|Integer> v =`Float|Integer`;Здесь v — объект метамодели, который мы можем проинспектировать. Например мы можем создать экземпляр, если это класс, мы можем вызвать функцию с параметром, если это функция, мы можем получить значение, если это атрибут, мы можем получить список классов, если это пакет и т.д. При этом справа от v стоит не строка, и компилятор проверит, что мы правильно сослались на элемент программы. То есть в Ceylon мы по существу имеем типобезопасную рефлексию. Соответственно благодаря метамодели мы можем написать весьма гибкие фреймворки.
Для примера, найдем средствами языка, без привлечения сторонних библиотек, все экземпляры класса в текущем модуле, которые имплементят определенный интерфейс:
shared interface DataCollector {}
service(`interface DataCollector`)
shared class DataCollectorUserV1() satisfies DataCollector {}
shared void example() {
{DataCollector*} allDataCollectorsImpls = `module`.findServiceProviders(`DataCollector`);
}Соответственно достаточно тривиально реализовать такие вещи, как инверсия зависимостей, если нам это реально нужно.
#25 Общий дизайн языка
На самом деле, сам язык весьма строен и продуман. Многие достаточно сложные вещи выглядят интуитивно и однородно
Рассмотрим, например, синтаксис прямоугольных скобок:
[] unit = [];
[Integer] singleton = [1];
[Float,Float] pair = [1.0, 2.0];
[Float,Float,String] triple = [0.0, 0.0, "origin"];
[Integer*] cubes = [ for (x in 1..100) x^3 ];В Scala, эквивалентный код будет выглядеть следующим образом:
val unit: Unit = ()
val singleton: Tuple1[Long] = new Tuple1(1)
val pair: (Double,Double) = (1.0, 2.0)
val triple: (Double,Double,String) = (0.0, 0.0, "origin")
val cubes: List[Integer] = ... В язык очень органично добавлены аннотации synchronized, native, variable, shared и т.д — это все выглядит как ключевые слова, но по существу это обычные аннотации. Ради аннотаций, чтобы не требовалось добавлять знак @ в Ceylon даже пришлось пожертвовать синтаксисом — к сожалению точка с запятой является обязательной. Соответственно Ceylon сделан таким образом, чтобы код, предполагающий использование уже существующих распространенных Java библиотеки вроде Spring, Hibernate, был максимально приятным для глаз.
Например посмотрим как выглядит исп��льзование Ceylon с JPA:
shared entity class Employee(name) {
generatedValue id
shared late Integer id;
column { lenght = 50;}
shared String name;
column
shared variable Integer? year = null;
}Это уже заточено на промышленное использование языка с уже существующими Java библиотеками, и здесь мы получаем весьма приятный для глаза синтаксис.
Посмотрим как будет выглядеть код Criteria API:
shared List<out Employee> employeesForName(String name) {
value crit = entityManager.createCriteria();
return
let (e = crit.from(`Employee`))
crit.where(equal(e.get(`Employee.name`),
crit.parameter(name))
.select(e)
.getResultList();
}По сравнению с Java мы здесь получаем типобезопасность и более компактный синтаксис. Именно для промышленных приложений типобезопасность очень важна. Особенно для тяжелых сложных запросов.
Итого, в данной статье мы играли на поле Ceylon и рассмотрели некоторые особенности языка, которые выгодно выделяют его на фоне конкурентов.
В следующей, заключительной части, попробуем поговорить не о языке как таковом, а об организационных аспектах и возможностях использовать Ceylon и других JVM языков в реальных проектах.
Ceylon on android https://www.youtube.com/watch?v=zBtSimUYALU
Ceylon swarm https://ceylon-lang.org/community/presentations/ceylon-swarm.pdf
