Продолжаем рассказ о языке цейлон. В первой части статьи С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 языков в реальных проектах.


Для заинтересованных еще немного интересных ссылок
Поделиться с друзьями
-->

Комментарии (29)


  1. senia
    20.06.2017 11:00

    Судя по описанию, кортежи в ceylon ближе к HList, чем к Tuple.
    В связи с чем вопрос: можно ли выполнить map по кортежу с сохранением информации о типах элементов?
    Примерно так:

    scala> (1, "str", 'sym) map elemToList
    res1: (List[Int], List[String], List[Symbol]) = (List(1),List(str),List('sym))
    

    Полный код по ссылке.


    1. elmal
      20.06.2017 11:46

      Нет, информация о типе не сохранится.


      {String+} mapped = [1, 1.0, "2.3"].map((Integer|Float|String element) => element.string);

      Результат Map — всегда Iterable. Iterable параметризуется одним типом. Здесь интереснее — в map приходит union тип, уже лямбда, передаваемая в map информацию о строгом типе теряет.


      1. senia
        20.06.2017 12:42

        Жаль. Возможность возвращать разный тип результата в зависимости от типа аргумента в HList::map позволяет делать очень интересные вещи.
        Например глубокое сравнение для case class:

        Foo(2, "foo") delta Foo(8, "bar")
        // 6 :: ("foo", "bar") :: HNil
        


      1. senia
        20.06.2017 12:52

        На всякий случай, чтоб мы говорили об одном и том же.
        Вот пример полиморфного метода: https://scalafiddle.io/sf/8Sq4LJv/0

        (1, 3.14, "str") map increase
        // (Int, Double, String) = (2,4.140000000000001,str + 1)
        


        1. elmal
          20.06.2017 13:53

          Я понял. Скорее всего для кортежей скорее всего можно извратиться. Но костылище будет еще то, не факт что будет иметь смысл такие извращения.


          Отдельный метод, принимающий кортеж, у которого обязательства вернуть кортеж строго с такими же типами. Внутри обработчик, принимающий union type, обрабатывающий типы и инкрементящий. И при возврате проверка, что тип кортежа не изменился.


          В Ceylon нет многого из того, что есть в Scala. В Kotlin аналогично :).


          1. senia
            20.06.2017 14:06

            строго с такими же типами

            Тут я все-таки опять не достаточно хороший пример привел. Тип результата не обязан совпадать с типом параметра. Совсем синтетический пример: https://scalafiddle.io/sf/apdsGEh/4.
            Также есть zip на HList, возвращающий строго типизированный HList и многие другие методы.
            Подобное требуется при метапрограммировании.


            1. elmal
              20.06.2017 14:22

              Осуществить подобное....


              Можно попробовать использовать обычный map. Вернется условно Seq[Object]. И далее можно этот Seq[Object] преобразовать снова к Tuple уже конкретных типов. Но к сожалению тип не получится вывести автоматом, мне придется делать что то ручного каста.


              Но вообще, фича запланирована:
              https://github.com/ceylon/ceylon/issues/5838
              Так что рано или поздно сделают.


  1. fillpackart
    20.06.2017 11:33

    Блин, выглядит круто!


  1. semmaxim
    20.06.2017 11:38
    +3

    Только вот в Kotlin есть 2 вещи, которые реально бесят.
    1. Функциональные методы на коллекциях (типа map, filter и т. д.) Iterable после каждого использования создают новый массив. Везде приходится прописывать сначала asSequence. Чем им не нравилась концепция «как в Scala» или хотя бы «как в C#» — без понятия. Очень бы хотелось посмотреть в невинные глазки этого «гения», что это придумал.
    2. Жутко засорено глобальное пространство имён. Все эти listOf, println и пр. — все доступны всегда и везде без всяких import.


    1. Sirikid
      20.06.2017 17:44
      +1

      Ну а в Java вы вместо asSequence пишите stream. Куда хуже было бы если трансформации строгих коллекций в строгом языке возвращали ленивую.


      1. semmaxim
        20.06.2017 21:33
        +1

        Не знаю, как в Java, но в C# и Scala ничего не пишу. И там и там эти методы ленивые и вычисляются на потоке элементов, один за одним. И только в Kotlin выпендрились и материализуют всю коллекцию (а это выделение динамического буфера под все объекты, и копирование всех элементов в него — очень нехилые по прожорливости операции) сразу после каждого метода в цепочке.


        1. Sirikid
          20.06.2017 23:14

          Ну а в Java вы вместо asSequence пишите stream.

          А может быть вот он ответ, у Kotlin ведь нету своих коллекций, только немножко статических методов (вроде asSequence) кое-где.


          1. semmaxim
            21.06.2017 14:08

            Как раз у Kotlin свои Iterable/List/Collection/Sequence.



  1. senia
    20.06.2017 12:31
    +5

    Раз уж в статье идет сравнение со scala, я позволю себе развить эту тему:
    18# Типы — объединения (union types)

    В большинстве языков программирования функция может может возвратить значения строго одного типа.
    Это если не учитывать наследование и ADT.
    Для возвращения ошибок есть Validation
    val p = Validated.catchNonFatal {
      Integer.parseInt("56")
    }
    

    Если же очень хочется именно объединения, то это потребует не намного больше кода:
    type F = Boolean :+: Double :+: String :+: Null :+: List[Nothing] :+: CNil
    def f(): F = {
      val rnd = util.Random.nextInt(5)
      rnd match {
        case 0 => Coproduct[F](false)
        case 1 => Coproduct[F](1.0)
        case 2 => Coproduct[F]("2")
        case 3 => Coproduct[F](null)
        case _ => Coproduct[F](List.empty)
      }
    }
    
    val v = f()
    
    v.select[Double].foreach { d => println(s"Double $d") }
    v.select[Int].foreach { i => println(s"Int $i") } // не скомпилируется, v не может быть Int
    

    Кстати, в ceylon можно ли обработать весь тип объединения полиморфным методом с сохранением типов?
    object headOption extends (List ~> Option) {
      def apply[T](l: List[T]) = l.headOption
    }
    
    val x = Coproduct[List[Int] :+: List[String] :+: CNil]("str" :: Nil)
    
    val res: Option[Int] :+: Option[String] :+: CNil = x map headOption
    
    res.select[Option[Int]].isEmpty
    // true
    res.select[Option[String]].nonEmpty
    // true
    


    19# Типы — пересечения (Intersection types)

    Это не проблема тех пор, как придумали наследование:
    trait CanRun {
        def run() = println("I am running")
    }
    
    trait CanSwim {
        def swim() = println("I am swimming")
    }
    
    trait CanFly {
        def fly() = println("I am flying")
    }
    
    case class Duck() extends CanRun with CanSwim with CanFly
    case class Pigeon() extends CanRun with CanFly {}
    case class Chicken() extends CanRun {}
    case class Fish() extends CanSwim {}
    
    def f(arg: CanFly with CanSwim) = {
        arg.fly(); 
        arg.swim();
    }
    
    f(Duck()); //OK Duck can swim and fly
    f(Fish()); //ERROR = fish can swim only
    


    20# Типы — перечисления (enumerated types)

    Обычно это называется ADT (algebraic data type).
    В scala тоже позволяет компилятору проверять полноту сопоставления с образцом.

    23# Алиасы типов (Type aliases)
    Или на класс, причем класс с конструктором:

    Интересный синтаксис, но мне кажется удобнее сделать ссылку на весь компаньон, чтоб получить все методы:
    type MyList[T] = List[T]
    val MyList = List
    
    val ml1: MyList[Int] = MyList.empty
    val ml2: MyList[Int] = MyList("str")
    


    21# Кортежи

    Выше уже отметил, что лучшим аналогом в scala является HList.

    22# Конструирование коллекций (for comprehensions)

    Многомерное итерирование поддерживатеся?
    val res = for {
     x <- 1 to 15 by 2
     if x % 3 == 0
     y <- 1 to 10 by 3
    } yield x -> y
    // Vector((3,1), (3,4), (3,7), (3,10), (9,1), (9,4), (9,7), (9,10), (15,1), (15,4), (15,7), (15,10))
    

    Это только для коллекций или можно обобщить на произвольную монаду?

    24# Улучшенные дженерики

    Вообще хорошо, но нельзя забывать, что это влечет некоторые проблемы при взаимодействии с java. Например с java библиотеками для сериализации в JSON.

    24# Метамодель

    Звучит очень хорошо. Это полноценный механизм макросов как в scala? Можно при помощи него генерировать новые классы во время компиляции?

    #25 Общий дизайн языка

    Дискуссионный момент. Кому-то (например мне) может больше нравиться подход scala, где сделан упор на расширяемость языка, что позволяет делать такие сторонние библиотеки как shapeless, cats и другие, привносящие в язык новые концепции.


  1. elmal
    20.06.2017 13:00
    +1

    22# Конструирование коллекций (for comprehensions)
    Многомерное итерирование поддерживатеся?

    Эквивалентный код:


        value res = {for (x in (1:15).by(2)) 
                     if (x % 3 == 0) 
                     for (y in (1:10).by(3)) x -> y}

    Да, это только для коллекций, точнее для того, что можно итерировать. Обобщить на произвольную монаду можно, если ее привести к коллекции :).


    24# Метамодель
    Звучит очень хорошо. Это полноценный механизм макросов как в scala? Можно при помощи него генерировать новые классы во время компиляции?

    Нет, даже близко не макросы. Ceylon не поддерживает вообще никакого Compile time метапрограммирования. Только рантайм. На самом деле я сделал в одном проекте кодогенерацию на основе метамодели, но это близко к костылю так как метод кодогенерации я должен сначала скомпилировать, потом дернуть, затем скомпилировать результат.


    24# Улучшенные дженерики
    Вообще хорошо, но нельзя забывать, что это влечет некоторые проблемы при взаимодействии с java. Например с java библиотеками для сериализации в JSON.

    На самом деле в основном тут с большим потреблением памяти. Когда в коллекции с дженериками потребовалось пихать миллиарды значений, это съело примерно в 2 раза больше памяти чем на Java коллекциях. Потому пришлось быстренько эти места переписать на Java. Кстати, именно с библиотеками для сериализации именно с улучшенными дженериками проблем то и нет :). Там в самом языке есть хинт — ee режим. Если нужно сериализовать классы, и они помечены определенными аннотациями, соответствующая коллекция будет для Java библиотеки выгляжеть как Java коллекция, и сериализация пройдет. С сериализацией есть действительно проблемы, но в принципе жить можно. Самая очевидная — иерархические класслоадеры. Сериализовать без проблем. А десериализовать — проблема, так как класслоадер библиотеки сериализации не видит классы, вызывающие эту библиотеку. Но есть хинт, ключ --flat-classpath, в результате чего класслоадер будет один и поведение как в Java/


    25 Общий дизайн языка

    Естественно дискуссионный. На самом деле Scala весьма и весьма крута, и это я постараюсь отразить в следующей статье, наброски которой у меня уже есть. По фичам для написания DSL со Scala тягаться весьма проблематично. Но к Scala нужно привыкать. Ceylon вполне читабелен даже без привыкания.


    1. senia
      20.06.2017 13:10

      Спасибо, интересно.

      22. Немножко громоздко, но можно привыкнуть. Я правильно понимаю, что вот для такого понадобятся дополнительные скобки?

      val res = for {
       x <- 1 to 15 by 2
       z = x*x -1
       if z % 3 == 0
       y <- 1 to 10 by 3
      } yield z -> y
      

      Future же к коллекции не привести, или я не прав? Да и потеря типа не приятна.
      Есть аналог async/await?
      Там, на самом деле более сложный вопрос — как в for работать с Future[Validation[...]] — для асинхронной обработки результата. В scala для этого monad transformers.

      24. Мне кажется больше проблем в десериализации generic типа — если экземпляр создан в java, то у него не будет метаинформации о конкретном типе с которым он создан. И ceylon код, рассчитывающий на эту информацию будет несколько удивлен.


      1. elmal
        20.06.2017 13:45

        1. Немножко громоздко, но можно привыкнуть. Я правильно понимаю, что вот для такого понадобятся дополнительные скобки?

        Вот для такого на самом деле в текущей версии языка придется делать кое что покруче чем скобки:


            value res = {for (x in (1..15).by(2))
                         for (z in {x * x - 1})
                         if (z % 3 == 0)
                         for (y in (1..10).by(3))
                         z->y};

        То есть для z пришлось имитировать итерированием по последовательности из одного элемента. В будущем обещают let разрешить внутри for comprehensions, пока так.


        Future по идее можно привести к коллекции из одного элемента. Примерно таким же способом, как я с z извратился.


        async await планируют в будущем, но не знаю сколько лет еще ждать. Я пока вместо asinc await просто rxJava использую. Плюс у меня есть библиотечный async метод, который стартует Java поток. Возвращающий Observable. Соответственно внутри потока могу стартовать еще с помощью async другие потоки, и далее у Observable вызываю blockingFirst. Пока не появится async await в языке — живу вот так.


        24

        На самом деле Ceylon достаточно неплохо работает с Java коллекциями и в принчипе взаимодействие для программиста незаметное. Правда пока можно нарваться на багу взаимодействия с Java, в результате чего может не скомпилиться и придется искать обходные пути, фича новая и не все крайние условия пойманы. Касается баги в основном преобразования цейлоновских лямбд в Java лямбды — часто это работает, а если не работает приходится по старинке генерировать анонимный класс что громоздко.


  1. potan
    20.06.2017 17:09

    Ни когда не мог понять, зачем нужны union types, если есть алгебраические типы (они же case class/data class).
    Помимо того, что в них лучше контролируются типы, так еще и позволяют различать созданные с разной целью величины одного типа. А в типе String|String не поймешь, правая строка или левая.


    1. elmal
      20.06.2017 17:15

      Во первых, case class и data class как раз и нету :). Типа String|String быть не может — он сворачивается просто в String


      1. senia
        20.06.2017 18:21

        Кстати, это отлично иллюстрирует, что использовать объединения в качестве Validation нельзя. Метод getStoredError будет иметь тип Error | Error.


    1. senia
      20.06.2017 17:33
      +2

      Иногда создавать ADT либо излишне, либо невозможно.

      Излишне это бывает, например, в Validation, когда комбинируются ошибки из разных источников. Может оказаться, что каждый второй метод требует специального ADT под тип ошибки, да еще и мапить ошибки постоянно. С объединениями же можно написать так:
      Validated[Error1 | Error2, Result].

      В Dotty собираются ввести nullable типы следующим образом: T? становится синонимом для T | Null. Это тоже весьма удобно и не влечет потери в производительности.

      Но как основную причину появления объединений в Dotty я слышал то, что в некоторых случаях при попытке свернуть тип выражения (например 2 веток if) компилятор упирался в бесконечные или просто невыразимые типы, так что объединения упростили компилятор и дизайн языка.


    1. Sirikid
      20.06.2017 18:02

      Можно сказать что ADT это union types с тегами:


      -- Обычный Either
      data Either a b = Left a | Right b
      
      -- Через union type (синтаксиса a | b на самом деле в Haskell нет)
      data TagLeft = Left
      data TagRight = Right
      type Either a b = (TagLeft, a) | (TagRight, b)


      1. potan
        20.06.2017 18:12

        Можно. Только компилятору будет сложнее отследить правильность использования с учетом вывода типов.


  1. michael_vostrikov
    21.06.2017 14:43

    union types

    Подумалось, что причина появления языков типа PHP, где можно "вернуть любой тип из функции", это на самом деле желание вернуть не любой, а один из заранее заданного набора типов. Просто об этом не подумали или не захотели реализовать.


  1. tim-zh
    27.06.2017 19:10
    +3

    1. Очень жаль, что и в Ceylon, и в Kotlin под деструктуризацией понимается только деструктуризация кортежей.
    2. Не совсем понятно, как связаны union types и перегрузка операторов.
    3. В Scala, эквивалентный код будет выглядеть следующим образом:
      val singleton: Tuple1[Long] = new Tuple1(1)
      

      Это неправда, идеоматический вариант:
      val singleton = (1)
      

      new Tuple1 на практике не встречается.


    1. elmal
      27.06.2017 19:18

      1) Есть еще деструктуризация Entry, то есть пар ключ значение
      2) Виноват, ошибся, имел в виду перегрузку методов или функций
      Связано просто, пишем:


      void overloaded(Integer|Float param) {
      }

      Вместо


      void overloaded(Integer param) {}
      void overloaded(Float param) {}