πŸ“œ ⬆️ ⬇️

Why you should completely switch to Ceylon or Kotlin (part 2)

We continue the story about the language of Ceylon. In the first part of the article, eylon appeared as a guest on the Kotlin field. That is, they took the strengths and tried to compare them with Ceylon.


In this part, Ceylon will act as a host, and list those things that are close to unique and which are the strengths of Ceylon.


Go:


18 # Types - unions (union types)


In most programming languages, a function can return values ​​of exactly one type.


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(); 

What is the real type of v? In Kotlin or Scala, the type will be Object? or simply Object, as the most common possible.


In the case of Ceylon, the type will be Boolean | Float | String | Null | [] | ComplexNumber.


Based on this knowledge, if we, let's say, try to execute the code


 if (is Integer v) { ...} // , v       if (is Float v) { ...} //  

What does this give in practice?


First, let's remember about checked exceptions in Java. In Kotlin, Scala and other languages, for several reasons, they were abandoned. However, the need to declare that a function or method can return some erroneous value is not gone anywhere. Like nowhere did the need oblige the user to somehow handle the error situation.


Accordingly, you can, for example, write:


 Integer|Exception p = Integer.parse("56"); 

And then the user is obliged to somehow handle the situation when an exception is returned instead of p.
You can run the code, and further throw an exception:


 assert(is Integer p); 

Or we can handle all possible options via switch:


 switch(p) case(is Integer) {print(p);} else {print("ERROR");} 

All this allows you to write a fairly concise and reliable code, in which many potential errors are checked at the compilation stage. Based on the experience of using, union types - this is really very convenient, very useful, and this is something that is very lacking in other languages.


Also due to the use of union types, in principle, you can live without the functional overloading functions. In Ceylon, this was removed in order to simplify the language, due to which very clean and simple lambdas were achieved.


19 # Types - Intersections (Intersection types)


Consider the code:


 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 

We have declared a function that takes as its argument an object that must simultaneously be able to fly and float.


And we can call the appropriate methods without problems. If we pass a Duck class object to this function, everything is fine, as the duck can fly and swim.


If we pass an object of the class Fish - we have a compilation error, because the fish can only swim, but not fly.


Using this feature, we can catch many errors at the time of compilation, while we can use many useful techniques from dynamic languages.


20 # Types - Enumerations (enumerated types)


You can create an abstract class, and which can be strictly specific heirs. For example:


 abstract class Point() of Polar | Cartesian { // ... } 

As a result, you can write handlers in switch without specifying 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); } } 

In case if we add another subtype in the course of the product development, the compiler will find for us those places where we missed the explicit treatment of the added type. As a result, we will immediately catch errors at the time of compilation and IDE will highlight them.


With this functionality, Ceylon makes an analogue of enum in 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") {} 

As a result, we get the enum functional with little or no additional abstractions.
If you need a functionality similar to valueOf in Java, we can write:


 shared Animal fromStrToAnimal(String name) { Animal? res = `Animal`.caseValues.find((el) => el.name == name); assert(exists res); return res; } 

The corresponding variants of enum can be used in switch, etc., which in many cases helps to find potential errors at the time of compilation.


23 # Type aliases aliases


Ceylon is a language with very strict typing. But sometimes the type can be quite cumbersome and confusing. To improve readability, you can use type aliases: for example, you can make an interface alias, thereby eliminating the need to specify a generic type:


 interface People => Set<Person>; 

For union or intersection types, a shorter name can be used:


 alias Num => Float|Integer; 

Or even:


 alias ListOrMap<Element> => List<Element>|Map<Integer,Element>; 

You can make aliases to the interface:


 interface Strings => Collection<String>; 

Or a class, moreover a class with a constructor:


 class People({Person*} people) => ArrayList<Person>(people); 

A class alias to the tuple is also planned.


Due to aliases, it is possible in many places not to produce additional classes or interfaces and to achieve greater readability of the code.


21 # Tuples


Ceylon has very good support for tuples, they are organically built into the language. In Kotlin considered that they are not needed. In Scala, they are made with size restrictions. In Ceylon, tuples are a linked list, and accordingly can be of arbitrary size. Although in reality the use of tuples from a variety of different types of elements is a very controversial practice, rather long tuples may be needed, for example, when working with rows of database tables.


Consider an example:


 value t = ["Str", 1, 2.3]; 

The type will be quite readable - [String, Integer, Float]


And now the most delicious thing is destructuring. If we get a tuple, then we can easily get specific values. The convenience syntax is pretty much the same as in Python:


 value [str, intVar, floatType] = t; value [first, *rest] = t; value [i, f] = rest; 

Destructuring can be done inside lambda, inside switch, inside for - it looks quite readable. In practice, due to the functionality of tuples and destructurization, in many cases, it is possible to abandon the functionality of classes with little or no damage to the readability and type safety of the code, this allows you to write prototypes very quickly, as in dynamic languages, but make fewer errors.


22 # Constructing collections (for comprehensions)


A very useful feature that is difficult to refuse after it has mastered. Let's try to iterate from 1 to 25 with a step of 2, excluding the elements dividing without a remainder by 3 and we will square them.


Consider the python code:


 res = [x**2 for x in xrange(1, 25, 2) if x % 3 != 0] 

On Ceylon, you can write in a similar style:


 value res = [for (x in (1:25).by(2)) if ( (x % 3) != 0) x*x]; 

You can do the same thing lazily:


 value res = {for (x in (1:25).by(2)) if ( (x % 3) != 0) x*x}; 

Syntax works including collections:


 value m = HashMap { for (i in 1..10) i -> i + 1 }; 

Unfortunately, there is no way to design Java collections so elegantly. While out of the box, the syntax will look like:


 value javaList = Arrays.asList<Integer>(*ObjectArray<Integer>.with { for (i in 1..10) i}); 

But writing functions that construct Java collections can be done very quickly. The syntax in this case is as follows:


  value javaConcurrentHashMap = createConcurrentHashMap<Integer, Integer> {for (i in 1..10) i -> i + 1}; 

22 # Modularity and Ceylon Herd
Long before the release of Java 9, there was modularity in Ceylon.


 module myModule "3.5.0" { shared import ceylon.collection "1.3.2"; import ceylon.json "1.3.2"; } 

The module system is already integrated with maven, so dependencies can be imported using traditional tools. But in general, Ceylon is recommended not to use Maven artifactories, but Ceylon Herd. This is a separate server (which can also be deployed locally) that stores artifacts. Unlike Maven, here you can immediately store the documentation, as well as Herd checks all the dependencies of the modules.


If everything is done correctly, it turns out to get away from jar hell, which is very common in Java projects.
By default, modules are hierarchical; each module is loaded through the Class Loaders hierarchy. As a result, we get protection that one class will be on the same path in ClassPath. You can enable behavior, as in Java, when the classpath is flat β€” this happens when we use Java libraries for serialization. For when deserializing, the ClassLoader library will not be able to load the class into which we deserialize, since the serialization library module does not contain dependencies on the module in which the class into which we deserialize is defined.


24 # Improved generics


Ceylon has no Erasure. Accordingly, you can write:


 switch(obj) case (is List<String>) {print("this is list of string)}; case (is List<Integer>) {print("this is list of Integer)}; 

It is possible for a specific method to find out in runtime type:


 shared void myFunc<T>() given T satisfies Object { Type<T> tclass = `T`; //some actions with tClass 

There is support for self types. Suppose we want to make the interface Comparable, which is able to compare an element both with itself and with another element. We will try to limit the types of traditional means:


 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 } } 

Did not work out! One-way compareTo works without problems. And the other does not work!


And now apply the self types functionality:


 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); } } 

Everything compiles, we can compare objects of exactly the same type, it works!


Also, for generics, a much more compact syntax, support for covariance and contravariance, there are default types:


 shared interface Iterable<out Element, out Absent=Null> ... 

As a result, we again have a much better and stronger typing compared to Java. However, it is not recommended to use some features in the bottlenecks of the program, receiving information about types in runtime is not free. But in places that are not critical for speed, this can be very useful.


24 # Metamodel


At Ceylon, we can run quite a lot of program elements in detail in runtime. We can inspect the fields of a class, we can inspect an attribute, a specific package, a module, a specific generic type, and so on.


Consider some options:


 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`; 

Here v is the object of the metamodel that we can inspect. For example, we can create an instance, if it is a class, we can call a function with a parameter, if it is a function, we can get a value, if it is an attribute, we can get a list of classes, if it is a package, etc. In this case, the right of v is not a string, and the compiler will check that we correctly referred to the program element. That is, in Ceylon, we essentially have type-safe reflection. Accordingly, thanks to the metamodel, we can write very flexible frameworks.


For example, let's find the language tools, without the involvement of third-party libraries, all instances of a class in the current module that implement a specific interface:


 shared interface DataCollector {} service(`interface DataCollector`) shared class DataCollectorUserV1() satisfies DataCollector {} shared void example() { {DataCollector*} allDataCollectorsImpls = `module`.findServiceProviders(`DataCollector`); } 

Accordingly, it is rather trivial to implement such things as dependency inversion, if we really need it.


# 25 General language design


In fact, the language itself is very slender and thought out. Many rather complex things look intuitive and uniform.


Consider, for example, the syntax of square brackets:


 [] 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 ]; 

In Scala, the equivalent code would look like this:


 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 synchronized, native, variable, shared annotations are very organically added to the language - it all looks like keywords, but in essence these are regular annotations. For the sake of annotations, in order not to add the @ sign to Ceylon, you even had to sacrifice the syntax - unfortunately the semicolon is mandatory. Accordingly, Ceylon is made in such a way that the code that involves the use of existing existing Java libraries like Spring, Hibernate, is as pleasing to the eye as possible.


For example, let's see how Ceylon usage with JPA looks like:


 shared entity class Employee(name) { generatedValue id shared late Integer id; column { lenght = 50;} shared String name; column shared variable Integer? year = null; } 

This is already sharpened on the industrial use of the language with already existing Java libraries, and here we get a very pleasant syntax for the eyes.


Let's see how the Criteria API code will look like:


 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(); } 

Compared to Java, we get type safety and more compact syntax here. For industrial applications, type safety is very important. Especially for heavy complex queries.


So, in this article we played on the field of Ceylon and considered some of the features of the language, which favorably distinguish it from the competition.


In the next, final part, we will try to talk not about the language as such, but about the organizational aspects and possibilities of using Ceylon and other JVM languages ​​in real projects.


For those interested in some more interesting links

')

Source: https://habr.com/ru/post/330796/


All Articles