log

taiar

Ceylon Programming Language - Usage

This is the second of a post series where I’m going to talk about some new programming languages with a nice future ahead. During this series I’ll explore deeper the Ceylon, Dart, Elixir, Rust and Swift 2 languages.

The plan is to make a post a week until December 15 (or maybe a little more) and spend 2 weeks exploring each one of them. In the first week I’ll explore the general aspects of the language and make some comparisons with other very known and established languages. In the post of the second week I’ll go deeper inside each one of the languages and explore the individual advantages on use them.

All the posts

  1. Ceylon Introduction
  2. Ceylon Usage
  3. Dart Introduction
  4. Dart Usage
  5. Elixir Introduction
  6. Elixir Usage
  7. Rust Introduction
  8. Rust Usage

Preface

There are hundred of programming languages out there. Which one should we use? Which help do we have to choose well? How do they compare to each other? This document is an attempt to provide some answers to these questions. Naturally, it would not be possible to provide complete answers: as I mentioned, there are too many programming languages. Nevertheless, we chose five languages with a potential to grow in importance in the coming years. These programming languages are Elixir, Rust, Dart, Swift and Ceylon. During this project, we shall be talking about each one of them. These discussions will be in breath, not in depth. Their goal is to provide the reader with the minimum of information necessary to compare them, and who knows, to lure one or other interested person in learning them in a greater level of details. In any case, we hope to contribute a bit to the popularization of these programming languages, which - likely - will be paramount to the development of computer science in the next ten years.

Ceylon example

To show some more interesting features of Ceylon, I wrote a version of the Producer-consumer problem. This is a classic example of a multi-process synchronization problem. It describes two processes, the producer and the consumer, who share a common, fixed-size storage (buffer) used as a queue. The producer’s job is to generate a piece of data, put it into the storage and start again. At the same time, the consumer is consuming the data (i.e., removing it from the storage) one piece at a time. The problem is to make sure that the producer won’t try to add data into the storage if it’s full and that the consumer won’t try to remove data from an empty storage. 1

The implementation relies strongly of the Java Thread libraries. The Producer and the Consumer objects runs on different threads, each instance of each object. The Storage class manage the additions of the producers and the remotions of the consumers on the buffer. For synchronicity I used the Java’s Semaphore class.

Let’s see the code and I’ll give the explanations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import ceylon.collection { ArrayList }
import java.util.concurrent { Semaphore }
import java.lang { Thread, Math }

shared Integer getRandomInteger(Integer a, Integer b) {
  value range = b - a + 1;
  value fraction = (range * Math.random());
  return (fraction + a).integer;
}

class Producer(Storage storage, shared actual Integer id) extends Thread() {

  void produce() {
      if(Math.random() < 0.5) {
          storage.add(this);
      }
  }

  shared actual void run() {
      while(true) {
          this.produce();
          this.sleep(getRandomInteger(1, 4) * 1000);
      }
  }

}

class Consumer(Storage storage, shared actual Integer id) extends Thread() {

  void consume() {
      if(Math.random()  < 0.5) {
          storage.get(this);
      }
  }

  shared actual void run() {
      while(true) {
          this.consume();
          this.sleep(getRandomInteger(1, 4) * 1000);
      }
  }

}

class Storage(shared Integer storageSpaces) {

  value buffer = ArrayList<Integer>(storageSpaces);
  variable Integer lastEmpty = 0;
  value m = Semaphore(1);

  for(i in 0..(this.storageSpaces - 1)) {
      this.buffer.push(0);
  }

  shared void get(Producer|Consumer actor) {
      m.acquire();
      assert(actor is Consumer);
      print("[Consumer " + actor.id.string + "] Wants to consume!");
      if(this.lastEmpty == 0) {
          print("[Storage] I have nothing for you now. Look: ");
      } else {
          this.lastEmpty--;
          this.buffer.set(this.lastEmpty, 0);
          print("[Storage] Ok, I got a thing for you.");
      }
      m.release();
      this.printBuffer();
  }

  shared void add(Producer|Consumer actor) {
      m.acquire();
      assert(actor is Producer);
      print("[Producer " + actor.id.string + "] Produced something.");
      if(this.lastEmpty == this.storageSpaces) {
          print("[Storage] I'm full! Can't take it right now, look:");
      } else {
          this.buffer.set(this.lastEmpty, 1);
          this.lastEmpty++;
          print("[Storage] Tank you! I'll store it.");
      }
      m.release();
      this.printBuffer();
  }

  shared void printBuffer() {
      m.acquire();
      process.write("[ ");
      for (load in this.buffer) {
          process.write(load.string + " ");
      }
      print("]");
      m.release();
  }

}

shared void run() {
  value storage = Storage(15);

  value p1 = Producer(storage, 1);
  value p2 = Producer(storage, 2);

  value c1 = Consumer(storage, 1);
  value c2 = Consumer(storage, 2);

  p1.start();
  p2.start();

  c1.start();
  c2.start();
}

The first thing we see are the imports. We are already used to them, in the first article where I used already some Java interoperability and explained the concept of modules. Every of these things are used here.

Toplevel functions

After that, we can se a function called getRandomInteger. In Ceylon, this is a toplevel function. A toplevel function declaration, or a function declaration nested inside the body of a containing class or interface, may be annotated shared (in the last article, I talked about the shared annotation and the visibility issues). A toplevel shared function is visible wherever the package that contains it is visible.

Another interesting thing about toplevel functions in Ceylon is that they can be called by external programs on the system. On the example, I’ll modify the function a little so we can see what is going on:

1
2
3
4
5
6
7
shared Integer getRandomInteger(Integer a = 1, Integer b = 5) {
  value range = b - a + 1;
  value fraction = (range * Math.random());
  value gen = (fraction + a).integer;
  print(gen);
  return gen;
}

The toplevel function must have no parameters (or default value parameters) so you can call her externally. By placing our program inside a module called prodcons (see the module session on the first article), we can compile and run the program like:

1
2
3
ceylon compile

ceylon run --run prodcons::getRandomInteger prodcons

The random integer numbers between [1, 5[ will be generated by the program and printed on the screen.

Classes

We have then, the class Producer which is the very same class of the Consumer, except it calls different methods of the Storage class. The first interesting thing we can see is that Ceylon 1.1 has no constructor methods. Since the earliest versions of the language, it supports a streamlined syntax for class initialization where the parameters of a class are listed right after the class name, and initialization logic goes directly in the body of the class.2

We can instantiate the class Producer like this:

1
value prod = Producer(Storage(15), 1);

The ability to refer to parameters of the class directly from the members of the class has the intuit to cut down on verbosity. However, there are moments when we would really appreciate the ability to write a class with multiple initialization paths, something like constructors in Java. The constructors are being implemented on Ceylon and will be available in the next versions of the language.

The annotation shared on the parameter id says that this is like a Java’s public member of the class. storage is not annotated, so it is like a private one.

Look at the annotation actual that is used in the same id parameter and in the run method. It tells that, inside the inheritance tree of possible values (since the two classes extends the Thread Java class) that could overwrite the method or the variable, this is the very one that will do it. So, shared id is overwriting the parameter id (probably on the Thread Java class) and shared run is overwriting a run method (surely on the Thread Java class). In the case of Interfaces, the word to tell that a class implements (from Java) an interface is satisfies; so a class satisfies an interface. The annotation actual is used to tell what method is satisfying the Interface’s specification.

Collections, sequences and tuples

Ceylon SDK has a great library that implements every kind of collections, just like Java does. There are interfaces and classes to implement all sort of operations involving ArrayList, LinkedList, PriorityQueue, HashSet, HashMap, TreeSet, TreeMap etc.3 In our example, I used an ArrayList (wich is the implementation of a list using arrays) to store the production of the Producer.

In the example, I used a loop to initialize every cell of the buffer list with the value zero. In the for loop, I used a Sequence to generate the iterable value. Sequence is a type that in the first time could look very familiar to a Java array but in fact they are very different. First of all, the sequence is a immutable type and not a mutable concrete type like the array. We can’t set the value of an element like:

1
2
String[] operators = .... ;
operators[0] = "^"; //compile error

This following code, doesn’t compile too:

1
2
3
4
for (i in 0..operators.size-1) {
    String op = operators[i]; //compile error
    // ...
}

The index operation operators[i] returns an optional type String?, which cannot be assigned to the type String. Instead, if we need access to the index, we use the special form of for:

1
2
3
for (i -> op in operators.indexed) {
    // ...
}

Ceylon has the tuple type too. It might be a very common use for the most of those who already worked with a language that has tuples.

1
[Float,Float,String] point = [0.0, 0.0, "origin"];

Type system

Every value in a Ceylon program is an instance of a type that can be expressed within the Ceylon language as a class. The language does not define any primitive or compound types that cannot, in principle, be expressed within the language itself.

Each class declaration defines a type. However, not all types are classes. It is often advantageous to write generic code that abstracts the concrete class of a value. This technique is called polymorphism. Ceylon features two different kinds of polymorphism:

  • subtype polymorphism, where a subtype B inherits a supertype A, and
  • parametric polymorphism, where a type definition A is parameterized by a generic type parameter T.

Ceylon, like Java and many other object-oriented languages, features a single inheritance model for classes. A class may directly inherit at most one other class, and all classes eventually inherit, directly or indirectly, the class Anything defined in the module ceylon.language, which acts as the root of the class hierarchy.

In our example, we use a parameter of the methods add and get which is Producer|Consumer type. This type means that, whateaver a Producer or a Consumer parameter passed the this method, it’ll work. The methods doesn’t need this, I place'em there just for exemplification. In Ceylon, this is called Union types. For any types X and Y, the union, or disjunction, X|Y, of the types may be formed. A union type is a supertype of both of the given types X and Y, and an instance of either type is an instance of the union type.

Assertions and exceptions

The assert statement validates a given condition, throwing an AssertionException if the condition is not satisfied. A distinguishing characteristic of Ceylon is that exceptions aren’t used to represent programming errors. The Ceylon creators thinks that exceptions like NullPointerException, ClassCastException and IndexOutOfBoundsException should never occur in at runtime in a production system. They represent problems that must be fixed by the programmer editing code, tend to hide the “corner” condition they represent from someone reading the code and are much too low-level to carry any useful information about the real problem. Because of that, Ceylon tries to encode these “corner” conditions into the type system. The compiler won’t let you write:

1
print(process.arguments[1].uppercased);

This code isn’t well-typed because process.arguments[1] is of type String?, reflecting the fact that there might not be a second element in the list process.arguments. Instead you’re forced to at least take into account the possibility that there are less than two arguments:

1
2
3
4
5
6
if (exists arg = process.arguments[1]) {
    print(arg.uppercased);
}
else {
    throw Exception("missing second argument");
}

Obviusly, the code is a little bigger than the initial code we had but of course the problem is very much clear, semanthic and the readers of the code would understand the problem behind the size of the arguments in a much clearer way.

Concurrency

In our example, inside of the methods get and add of the Storage class is where we would have concurrency problems.1 To solve this potential problems, the class uses syncronization implemented with semaphores. I used the Semaphore class from Java “acquiring” and “releasing” the execution flow whateaver some of the Threads are updating the buffer or printing in the screen.

In Java is very common to use the synchronized method annotation to tell a Thread that this method have race conditions, and the JVM takes care of the concurrency. To use it with Ceylon, I had to especifically put the call to the syncronization methods because it doesn’t have the annotation nor any kind of compatibility with it.

Quotes

References