Abb 1: Ceylon im Zusammenspiel

Kaum zu glauben, Gavin King und die Entwickler Community rund um Ceylon haben ihr Vorhaben durchgezogen und sind mit einem produktionsreifen Release der neuen Programmiersprache Ceylon an den Start gegangen. In diesem Artikel werden die Sprache und das Ökosystem von Ceylon dargestellt.

Das Ceylon Projekt ist eine Entwicklung von Fans des Java Ökosystems, welche besonders dessen Entwickler Community, praktische Orientierung, Kultur der Offenheit und Einsatz für Portabilität wert schätzen. Die rund 15 Jahre alte Sprache Java an sich ist jedoch für das Ceylon Team Grund genug gewesen eine Alternative zu erstellen. Das Große Ziel ist es also aufzuräumen und zu vereinfachen um für aktuelle Anwendungsfälle ausgerüstet zu sein. Dabei sollen sich Entwickler nicht in der Anwendung von Frameworks verlieren und gleichzeitig wieder Spaß beim Coden haben.

Ceylon verfolgt keinen spezifischen Anwendungsfall, sondern versteht sich als vielseitig einsetzbares Werkzeug. Syntaktisch lehnt sich Ceylon an C# und Java an. Dementsprechend sollte für viele Entwickler die Einarbeitung tendenziell einfacher sein. Rein formal lässt sich Ceylon wie folgt klassifizieren: imperativ, statisch typisiert, block-strukturiert, objektorientiert, höhere Ordnung. Das statische Typsystem wird in Ceylon besonders ernst genommen und könnte den Freunden von untypisierten Sprachen eventuell noch weniger Freude bereiten als in Java. Die strikte Typsicherung hebt den Nutzen von Typisierung allerdings auch hervor und könnte somit wiederum auch für Gegner des lückenhaften Typsystems von Java interessant werden. Die höhere Ordnung beschreibt ein Phänomen, welches aus funktionalen Sprachen oder den Neuerungen aus Java 8 bekannt ist. Gemeint ist damit, dass jedes Element der Sprache wie beispielsweise Attribute oder Operationen ebenfalls als Werte repräsentiert werden und auch darüber im Zugriff stehen.

Noch plattformunabhängiger

Mögliche Laufzeitumgebungen für Ceylon können die Java Virtual Machine (JVM) oder jede beliebige JavaScript Virtual Machine sein wie beispielsweise ein Browser oder die Node.js Plattform. Dabei ist Ceylon Code bezogen auf Java und JavaScript Code bidirektional interoperabel. Die Verbindung von Java und Ceylon übernimmt der Ceylon Compiler, welcher Java kompilieren kann, der Ceylon Code aufruft und vice versa. Die Integration mit JavaScript ermöglicht auch hier ein Compiler, welcher die Umwandlung von Ceylon Modulen in JavaScript Code übernimmt. Für die andere Richtung unterstützt Ceylon einen speziellen Modus in dem native JavaScript Funktionen aufgerufen werden können. Als gemeinsame Basis für die JVM und JavaScript stellt Ceylon ein SDK zur Verfügung, welches die Kernmodule in beiden Varianten bereitstellt. In Listing 1 ist die Verwendung eines einfachen Node.js HTTP Server dargestellt. Innerhalb der Request Verarbeitung wird myCelon.run() ausgeführt. MyCeylon ist an dieser Stelle eine Referenz auf von Ceylon kompilierten JavaScript Code (nodeceylon-1.0.0.js).

var http = require('http');
var myCeylon = require('./nodeceylon-1.0.0.js');
http.createServer(function (req, res) {
    res.end("Hallo, " + myCeylon.run());
}).listen(1337, '127.0.0.1');
Listing 1: Node.js to Ceylon
Zur Kompilierung der JavaScript Datei wurde das Ceylon Kommandozeilen-Tool verwendet, wie es in Listing 2 dargestellt ist.
$ ceylon compile-js –source=. nodeceylon
Listing 2: Kompilierung zu ceylon.js Code

Nodeceylon ist dabei ein einfaches Ceylon Modul, welches mindestens eine öffentliche Top-Level Operation besitzen muss. Zum Darstellen der Interoperabilität wurde ein einfacher String Wert zurückgegeben. (Siehe Listing 3)

shared String run() {
    return("Ceylon!");
}
Listing 3: Ceylon Methode

Die Ausgabe des Servers ist entsprechend “Hallo, Ceylon!”, wobei der letzte Teil des Strings aus einer zu JavaScript kompilierten Ceylon Operation kam.

Feingranulare Modularisierung

Bei der Entwicklung mit Ceylon fällt schnell auf, dass sich die Codestruktur hauptsächlich an Modulen orientieren muss. Die Module sollen typischerweise möglichst autark sein. Aus Entwicklungsperspektive muss man sich also mehr Gedanken über eine elegante und feingranulare Aufteilung machen. Ceylon Module sind nicht mit den Java Paketen zu vergleichen, welche in der Praxis hauptsächlich für den Namensraum von Klassen verwendet werden. Abhängigkeiten und Sichtbarkeit von Elementen werden in jedem Modul individuell konfiguriert. Was zunächst nach mehr Aufwand klingen kann, gestaltet sich in der Entwicklung von einfachen Prototypen als äußerst angenehm. Da es keine Zugriffsmodifikatoren wie public, private oder protected gibt, wird der Code wesentlich schlanker und leichter zu verstehen. In Ceylon findet die Kapselung hauptsächlich auf Modulebene statt. Um anderen Modulen Operationen zur Verfügung zu stellen müssen diese speziell mit dem Keyword „shared” deklariert werden.

Abb 2: Module & Beziehung

In Abbildung 1 sind zwei Module dargestellt. Modul A verfügt über keine modulfremden Abhängigkeiten. Klasse A ist ausschließlich von einer Operation in Klasse B abhängig, sodass diese die entsprechende Methode als “shared” deklarieren muss. In Modul B existiert eine Abhängigkeit zu Modul A, welche in Ceylon über einen Import in der module.ceylon Konfiguration des Moduls explizit gemacht werden muss. (Siehe Listing 4)

module modulB "1.0.0" {
    import modulA "1.0.0";
}
Listing 4: Modul-Dependency

Darüber hinaus muss in der Source Datei in der sich die Klasse Z befindet ein Import auf die konkrete Klasse A eingetragen werden. Dann kann entsprechend des Listings 5 die modulfremde Referenz verwendet werden.

import modulA { KlasseA }
class KlasseZ() {
    void methodeZ() {
        value objA = KlasseA();
        objA.methode1();
    }
}
Listing 5: Modulfremder Codezugriff

Die Implementierung von Modul A ist in Listing 6 dargestellt. In Modul A befindet sich die shared Klasse A mit einer shared Methode. Die Klasse B im selben Modul ist nicht shared, also nur in dem Modul sichtbar. Dennoch verfügt sie über eine shared Methode welche somit innerhalb des Moduls für andere Klassen sichtbar ist. Zusätzlich wurde eine Modul-Methode deklariert welche ohne “shared” nur innerhalb des Moduls verwendet werden kann.

shared class KlasseA() {
    shared void methode1() {
        methode2();
    }
    void methode2() {
        value obj = KlasseB();
        obj.methodeX();
        methodeModul();
    }
}
class KlasseB() {
    shared void methodeX() {
        methodeModul();
    }
}
void methodeModul() {}
Listing 6: Modulinterner Zugriff und öffentliche Methoden

Funktionale Programmierung

Rein formal ist Ceylon eine imperative Sprache und ist damit nicht von Grund auf funktional. Dennoch versucht sie Vorteile bei funktionalen Vorbildern abzuschauen und leitet Entwickler an unveränderliche Objekte zu verwenden, sowie Funktionen höherer Ordnung zu benutzen und sie als Argumente zu übergeben. Weitere funktionale Sprachkonstrukte wie Comprehensions führen diese Ansätze weiter und legen funktionalen Zucker auf die imperative Sprache.

Sicherheit zur Compilezeit

Für viele Entwickler ist jede Sicherheit zur Compilezeit Gold wert, da jeder Fehler welcher schon bei der Entwicklung von Code sichtbar ist und behoben werden kann, eine Rettung vor einem potentiellen Produktionsfehler ist. Bei Argumenten für typisierte Sprachen geht es selten um Entwicklungsgeschwindigkeit sondern um robuste Produktionssysteme und möglichst viel Sicherheit sowie Anleitung für unerfahrene Entwickler. Ähnlich sehen es die Entwickler von Ceylon, denn durch trickreiche Sprachkonstrukte werden Zugriffe auf Null-Referenzen und illegale Typumwandlungen bereits zur Compilezeit ausgeschlossen.

Variablen sind in Ceylon nullsafe. Alle Variablen müssen bei der Deklaration mit einem passenden Wert initialisiert werden. Möchte man den Wert Null zulassen, muss dieser explizit beim Datentyp angegeben werden, sodass der Compiler Kenntnis über die Möglichkeit hat, dass dieser Null sein kann. Im Gegenzug zwingt einen der Compiler bei einem Zugriff auf eine so deklarierte Variable zu einer vorgelagerten Prüfung auf Null.

Stand der Dinge

Ende letzten Jahres wurde das erste Release von Ceylon veröffentlicht. Enthalten war eine vollständige formale Spezifikation, Kommandozeilenwerkzeuge inklusive Java und JavaScript Compiler, sowie Unterstützung für die Entwicklung von Programmen für die JVM und Node.js. Es existiert eine brauchbare Eclipse Integration und ein Satz von 13 Sprachmodulen, welche in Tabelle 1 dargestellt ist.

Tabelle 1: Module des Ceylon SDK
Modulname JVM JavaScript Beschreibung
ceylon.collection Ja Ja Collection Modul
ceylon.dbc Ja - Datenkbank Modul
ceylon.file Ja - Datei Modul
ceylon.html Ja Ja HTML (Template) Modul
ceylon.interop.java Ja - Werkzeuge für Java / Ceylon Interop.
ceylon.io Ja - Stream, Scosk, Files Modul
ceylon.json Ja Ja JSON Parser / Serialisierungs-Modul
ceylon.math Ja - Mathematisches Modul
ceylon.net Ja - HTTP Modul (Client, Server)
ceylon.process Ja - API zum Starten nativer Prozesse
ceylon.test Ja Ja Testframework
ceylon.time Ja Ja Zeit Modul (angelehnt an JSR-310)
ceylon.unicode Ja - Unicode Modul

Wirft man einen Blick in den aktuellen Bugtracker [1] der zentralen Projekte von Ceylon und schaut sich die Anzahl der offenen und gefixten Bugs an, kann man einen stabilen Stand erkennen. (Siehe Tabelle 2) Die niedrige Gesamtanzahl an Bugs ist jedoch auf die geringe Verbreitung der Sprache zurückzuführen.

Tabelle 2: Bugtracker Snapshot
Modulname offen gefixt
ceylon-compile 37 883
ceylon-js 6 154
ceylon-common 8 31
ceylon-language 10 110
ceylon-sdk 6 33
Gesamt 67 1211

Neben den Modulen des SDKs existieren weitere Module, welche über ein zentrales Ceylon Modul Repository namens Ceylon Herd zur Verfügung gestellt werden. [2] Wenig überraschend ist der Blick in den Modulkatalog von Ceylon Herd, der verständlicherweise noch in den Kinderschuhen steckt. [3]

Typsystem

Das Typsystem von Ceylon ist ähnlich wie bei Java und C# hybrid. Es unterstützt Polymorphismus sowohl durch Vererbung und parametrisierten Typen. In Ceylon entspricht ein Typ entweder einer zustandslosen Schnittstelle, einer zustandsbehafteten Klasse, einem Typparameter oder einer Union bzw. Intersection. Anders als bei Java werden generische Typargumente nicht eliminiert. Damit entfallen mögliche Probleme durch die Typlöschung. Darüber hinaus gibt es in Ceylon keine primitiven Datentypen oder Arrays, d.h. alle Typen können innerhalb der Sprache selbst ausgedrückt werden. Obwohl die Sprache keine primitiven Datentypen kennt, kann der Compiler dennoch während einer Optimierung von primitiven Datentypen Gebrauch machen.

Union und Intersection Types

Mit Union Types werden mehrere Typen aus verschiedenen Zweigen der Typhierarchie zu einem Typ zusammengeführt. Der Union Type für die Typen String und Integer wird mit String | Integer beschrieben. Die Schnittmenge mehrerer Typen kann durch Intersection Types ausgedrückt werden. Beispielsweise kann für die Typen Addition und Multiplikation der Intersection Type Addition & Multiplikation definiert werden. Im Fall von disjunkten Typen entspricht der Intersection Type dem Typen Nothing.

Typ Aliase

Es ist oft sehr hilfreich kürzere bzw. zweckmäßigere Namen zu existierenden Typen anzubieten. Insbesondere bietet es sich an Typ Aliase bei parametrisierbaren Typen oder Union bzw. Intersection Typen anzuwenden. Die Verwendung von Typ Aliasen kann die produzierte Menge an Code verringern und zugleich zu ausdrucksstärkerem Code führen. Die Definition eines Typ Alias ähnelt dem typedef aus der Sprache C und erfolgt anhand des “=>” Zeichens.

interface Items => Set<Item>;
class Items({Item*} items) => ArrayList<Item>(items);
alias Zahl => Integer|Float|Decimal;
Listing 7: Typ Aliase

Typ Inferenz

Mit der Typ Inferenz entfallen explizite Angaben von Typen. Der Typ eines blocklokalen Wertes oder Funktion wird anhand ihrer Definition automatisch inferiert. Die Voraussetzung dafür ist die Verwendung des Schlüsselwortes value bzw. function statt der eigentlichen Typ Deklaration. Ebenso kann der Typ einer Variablen inferiert werden, welches Bestandteil einer Kontrollstruktur ist. Auch generische Typ Argumente können von Ceylon ohne Probleme inferiert werden. Allerdings beschränkt sich die Typ Inferenz auf lokale Deklarationen.

void typInferenz() {
    value text = "Hello, World!";
    value zahlen = { 1.0, 1.5, 2.0, 2.5 };
    function sqr(Integer a) => x^2;
    for (n in 0..m) { ... }
    for (s in ["A", "B", ...]) { print(s); }
}
Listing 8: Typ Inferenz

Typ Parameter und Varianz

Ceylon unterstützt keine Wildcard Typ Parameter wie man sie von Java kennt. Stattdessen kann ein Typ Parameter kovariant, kontravariant oder invariant sein. Ein kovarianter Typ Parameter kann nur als Rückgabetyp verwendet werden. Im Gegensatz dazu kann ein kontravarianter Typ Parameter nur als Eingabetyp innerhalb von Parameterlisten verwendet werden. Der Standard Zustand eines Typ Parameters ist invariant, d.h. es kann sowohl als Rückgabe- als auch Eingabetyp verwendet werden. Durch diese Systematik kann die Varianz eines Typ Parameters validiert werden.

interface Producer<out Value, in Rate>
given Value satisfies Object
given Rate of Float|Decimal { ... }
Listing 9: Typ Parameter und Varianz

Metamodel

Mithilfe der Metaprogrammierung können Typen, Funktionen oder Attribute zur Laufzeit referenziert und manipuliert werden. Anstatt eines vollständig integrierten Metamodells werden in Java Komponenten aus der Reflection API verwendet. Da die Benutzung dieser API häufig zu Laufzeitfehlern führen kann, setzt Ceylon auf eine stabilere Lösung. Das Metamodel von Ceylon ermöglicht es

Um in Ceylon eine Referenz auf ein Typ, eine Funktion oder einen Attribut zu bekommen werden Metamodel Expressions verwendet.Ein Metamodel Expression wird durch einen Typen innerhalb von Hochkommas dargestellt, wie z.B.: String. Der resultierende Typ einer Metamodel Expression hängt von der Art der Deklaration des referenzierten Elementes ab. Je nachdem kann es sich beispielsweise um eine Klasse, Schnittstelle, Funktion, oder Attribut handeln. Wird dem Typen vorweg noch das zugehörige Schlüsselwort angefügt, so erhält man eine Referenz auf die Deklaration des jeweiligen Programm Elements. Das Listing zeigt eine beispielhafte Anwendung von Metamodel Expressions. Hierbei werden die deklarierten Klassen und Schnittstellen des Moduls “meinmodul” ausgelesen. Die Verwendung von Compilezeit sicheren Bezeichnern anstatt von String Konstanten um Elemente zu referenzieren ist eine deutliche Verbesserung gegenüber der bekannten Reflection API aus Java.

value moduleMembers = `module meinmodul`.members;
for (m in moduleMembers) {
value interfaces = m.members<ClassDeclaration>();
    value classes = m.members<InterfaceDeclaration>();
    print("interfaces: ``{ for (i in interfaces) i.name }``");
    print("classes: ``{ for (c in classes) c.name }``");
}
Listing 10: Metamodel

Annotationen

Annotationen erlauben es dem Programmierer Informationen an Deklarationen anzufügen. Diese Informationen können dann zur Laufzeit mit Hilfe des Ceylon Metamodells abgefragt werden. Um eine Annotation zu erstellen muss ein Konstruktor in Form einer Funktion definiert werden. Mit dem Konstruktor wird der Typ der Annotation festgelegt. Mit dem Annotationstypen ConstrainedAnnotation kann die Verwendung einer Annotation eingeschränkt werden. Ceylon bietet dazu zwei mögliche Annotationstypen an, welche die meisten Anwendungsfälle abdecken sollten. Wenn eine Annotation an einem Programm Element höchsten einmal vorkommen darf eignet sich OptionalAnnotation. Darf ein Annotationstyp mehrfach vorkommen, so kann von SequencedAnnotation geerbt werden. Beide Annotationstypen verlangen die Definition der anwendbaren Programmelemente. Eine Reihe von erlaubten Programm Elementen kann dabei anhand von Union Types abgebildet werden. Der Typ ValueDeclaration | FunctionDeclaration würde beispielsweise die Anwendung einer Annotation sowohl für Attribute als auch für Funktionen erlauben.

shared annotation ValidateAnnotation validate()
    => ValidateAnnotation();

shared final annotation class ValidateAnnotation()
    satisfies OptionalAnnotation<ValidateAnnotation, ValueDeclaration> {}

class Data(Integer n1, Integer n2) {
    shared variable validate Integer validNumber = n1;
    shared variable Integer number = n2;
}

void save(Data data) {
    value values = `class Data`.declaredMemberDeclarations<ValueDeclaration>();
    for (v in values) {
        value validateAnnotation = v.annotations<ValidateAnnotation>();
        if (nonempty validateAnnotation) {
            // validate ...
        }
    }
    // save ...
}
Listing 11: Annotation

Syntaktischer Zucker

In Ceylon existieren für bestimmte Ausdrücke syntaktische Abkürzungen. In Tabelle 3 sind die Langformen mit den entsprechend optionalen Kurzschreibweisen dargestellt.

Tabelle 3: Liste der syntaktischen Kurzformen
Langform Kurzform
Callable<Boolean, [String]> Boolean(String)
Null|String String?
Iterable {String*}
Sequential [String*] oder nostalisch: String[]
Empty []
is Object x exists x
is [String+] x nonempty x
x>1 && x<u 1<x<u

Vermeidung von Methodenüberladung

Mit Hilfe von variadischen Parametern, welche eine unbestimmte Arität haben, und der Einführung von Default-Parametern, kommtCeylon ohne Methodenüberladung aus. Werden optionale Parameter benötigt, wird dafür entsprechend eine Default-Initialisierung in der Deklaration des Parameters angegeben. Anhand von Kardinalitätsangaben (Listing 12) können unbestimmte Mengen von Parametern realisiert werden.

void addItem(Product product, Integer quantity=1) { ... } // Default Parameter
String join(String* strings) { ... } // 0-n Strings
Listing 12: Vermeidung von Methodenüberladung

Fazit

Während die Interoperabilität mit JavaScript alles andere als ausgereift ist, bereitet die Entwicklung mit dem Metamodel bereits wesentlich mehr Freude und zeigt Ansätze welche eine strukturiertere und stabilere Framework Entwicklung ermöglichen können. Aus unserer Sicht ist dies momentan die größte Stärke von Ceylon und könnte zukünftig ein Hauptanwendungsfall werden. Eine weitere Stärke ist die feingranulare Modularisierung, zu der Ceylon einen anleitet. Dies spricht sicherlich für eine mögliche Verwendung von Ceylon in größeren Entwicklungsprojekten. Positiv fällt auch auf, dass der Code sehr knapp gehalten ist aber trotzdem sehr ausdrucksstark und eindeutig bleibt. Für ein schnelles ausprobieren können wir den Ceylon Webrunner empfehlen, der eine Browserintegration zum Entwickeln und Ausführen von Ceyloncode bietet. [4] Die Integration mit Node.js ist jedoch aufgrund von einigen Konflikten wie beispielsweise mit Express.js noch nichts für schwache Nerven. Im zukünftigen Ceylon Release 1.1 liegen die Schwerpunkte auf Performanceverbesserungen, Ausbau der IDE Integration und Freigabe der ceylon.transaction Module.

Referenzen

  1. Bugtracker Ceylon, http://ceylon-lang.org/code/issues/, Stand: 25.04.2014.  ↩

  2. Ceylon Herd, http://modules.ceylon-lang.org/, Stand: 25.04.2014.  ↩

  3. Ceylon Herd Module, https://modules.ceylon-lang.org/modules, Stand: 25.04.2014.  ↩