Einstieg in Crystal: Kompilierte Sprache mit modernen Konzepten
Die von Ruby inspirierte Sprache Crystal empfiehlt sich dank leichtem Einstieg und eigenem Compiler als performante Alternative für General-Purpose-Projekte.
Die Programmiersprache Crystal besitzt eine schnell zu erlernende, sehr stark von Ruby inspirierte Syntax und dazu noch die Möglichkeit, Applikationen direkt in Maschinencode zu kompilieren. Damit soll sie als General-Purpose-Sprache leistungsfähig genug für verschiedenste Aufgaben sein. Der eigene Compiler erspart Optimierungsmaßnahmen, wie sie beispielsweise mit JIT und Co. nötig sind – auch dann, wenn ein Crystal-Programm sehr hohe Performance erreichen soll. Entwicklerinnen und Entwickler, die gerne regelmäßig neue Programmiersprachen erlernen, finden in Crystal also eine interessante Option, die sich ausreichend von etablierten Sprachen absetzt.
Von JavaScript verbrannten Entwicklern dürfte außerdem positiv auffallen, dass Crystal am Ende doch streng typisiert ist – so kann der Compiler beispielsweise Nullchecks durchführen und so viele häufige Programmfehler schon während der Kompilation erkennen.
Für Crystal sprechen auch die umfangreichen Möglichkeiten der Metaprogrammierung. Darunter versteht man in der Welt von Crystal, dass sich Code selbst modifizieren beziehungsweise selbst generieren kann. Zwar ist die Flexibilität beim Kompilieren in nativen Code nicht so hoch wie bei zur Laufzeit interpretierten Sprachen, trotzdem gilt, dass Crystal Präprozessor-Lösungen, wie sie bei den mit C verwandten Sprachen weit verbreitet sind, haushoch überlegen ist.
Historie: Von Ruby inspiriert, aber schneller
Abkündigungen (Deprecations) von Komponenten in einem Ökosystem können für Entwicklerinnen und Entwickler dramatische Folgen nach sich ziehen. Während sich der Wegfall einer Bibliothek meist gut verkraften lässt, kommt die Abkündigung einer Programmiersprache in einem laufenden Softwareprojekt einem wirtschaftlichen Totalschaden gleich – die komplette Codebasis wird unbrauchbar, wie das "Abenteuer" um Visual Basic 6 [1] anschaulich gezeigt hat. Aus diesem Grund sind Entwickler und Entwicklerinnen gut beraten, sich nicht auf zu exotische Programmiersprachenprojekte einzulassen.
Die Programmiersprache Crystal wurde im Jahre 2011 von dem argentinischen Beratungsunternehmen Manas Technology Solutions gestartet. Die Entwickler Ary Borenszweig, Juan Wajnerman und Brian Cardiff ließen sich zunächst noch stark von Ruby inspirieren, suchten dann aber schnell nach Wegen, eine höhere Performance der resultierenden Systeme zu erzielen.
Vor knapp zwei Jahren hat Crystal die Reife eines Major Release 1.0 erreicht [2] und erfreut sich wachsender Beliebtheit. Rund um das GitHub-Repository [3] der unter Apache-2.0-Lizenz als Open Source verfügbaren Programmiersprache ist ein aktives Ökosystem mit mehr als 500 Beitragenden entstanden – und Crystal verzeichnet aktuell in Version 1.6.2 schon mehr als 18.000 GitHub-Sterne (s. Abb. 1).
Mit Crystal erstellte Programme funktionieren auf den Prozessorarchitekturen x86, x64 sowie den beiden ARM-Varianten (32 und 64 Bit) unter Linux, macOS und FreeBSD. Der Support für Windows ist zum Zeitpunkt der Fertigstellung dieses Artikels hingegen noch experimentell.
Installation von Crystal
Für die ersten einführenden Schritte in die Arbeit mit Crystal kommt im Folgenden Ubuntu 22.04 LTS zum Einsatz. Wer die Beispiele mit einem anderen Betriebssystem nachvollziehen möchte, findet auf der Crystal-Website [4] Installationsanweisungen für diverse Plattformen.
Für Ubuntu steht ein per cURL ausführbares Installationsskript parat, das sich folgendermaßen aktivieren lässt:
curl -fsSL https://crystal-lang.org/install.sh | sudo bash
Wie in der folgenden Ausgabe zu erkennen, lädt das Werkzeug eine aktuelle Nightly-Version von Crystal herunter:
crystal -v
Crystal 1.6.2 [879691b2e] (2022-11-03)
LLVM: 13.0.1
Default target: x86_64-unknown-linux-gnu
Über spezifische Parameter lassen sich alternativ aber auch stabile oder gezielt ausgewählte Crystal-Versionen über das Skript installieren. Weitere Informationen hierzu finden sich in der Installationsdokumentation [5].
Entwicklerinnen und Entwickler, die ihre Software lieber via snap beziehen, können auch auf diese Paketierungsvariante zurückgreifen. In den folgenden Beispielen wird darauf verzichtet, um Komplikationen mit AppArmor und Co. zu vermeiden.
Bei der Arbeit mit Crystal ist außerdem eine vollwertige Entwicklungsumgebung (IDE) empfehlenswert. In der Welt von Crystal hat sich Microsofts quelloffenes Visual Studio Code als Quasistandard etabliert. Es soll auch in den folgenden Schritten zum Einsatz kommen.
Nach dem Start von Visual Studio Code erscheint das Plug-in Crystal Language als rotierendes Icon. Ein Klick auf Install startet den bei Visual-Studio-Code-Erweiterungen typischen Deployment-Prozess.
Wer darüber hinaus IntelliSense nutzen möchte, muss zusätzlich das Language-Server-Modul installieren, das der IDE die Parsing-Informationen zur Verfügung stellt. Es findet sich samt Installationsanweisung derzeit im GitHub-Repo von Julien Elbaz (elbywan) [6].
"Hallo Welt" mit Crystal
Nach erfolgreichem Einrichten der Crystal-Arbeitsumgebung kann ein erstes "Hello World"-Programm starten. Wie für Crystal-Code üblich, ist dazu eine Datei mit der Endung .cr anzulegen und diese in einer schon laufenden Instanz von Visual Studio Code zu öffnen:
touch helloworld.cr
code helloworld.cr
Das Beispiel zeigt bereits einige Besonderheiten des Typsystems:
zahl1 = 20
zahl2 = 30
p "Hallo Welt", zahl1, zahl2
KONSTANTE = 24
zahl1, zahl2 = zahl2, zahl1
p "Hallo Welt", zahl1, zahl2
Wichtig beim Einsatz von Variablen in Crystal ist vor allem, dass Variablennamen immer mit einem Kleinbuchstaben anfangen müssen. Ist der erste Buchstabe der Variable ein Großbuchstabe, so stuft die Runtime sie als Konstante ein. Versuche, den Wert zu ändern, führen dann zu Kompilierungsfehlern.
Im obigen Listing fallen zwei Besonderheiten auf: Zum einen erfolgt die Ausgabe von Informationen in der Kommandozeile einfach durch Voranstellen von p
und einer Parameterliste. Zweitens veranlasst die Zeile zahl1, zahl2 = zahl2, zahl1
einen Tausch der in den Variablen gespeicherten Werte.
Zuweisungsoperationen mit mehreren Werten sind im Programmieralltag geläufig, weshalb Crystal hierfür eine syntaktische Besonderheit zur Verfügung stellt. Durch Ausführen des obigen Statements lässt sich das Austauschen in den gespeicherten Werten erreichen – wie sich durch Kompilieren respektive Ausführen des Programms überprüfen lässt:
crystal helloworld.cr
Wer dem Crystal-Compiler nur einen Dateinamen einer .cr-Datei als Parameter zur Ausführung übergibt, weist das Programm dazu an, eine Code-Datei direkt auszuführen. Das führt zum in Abbildung 3 gezeigten Ergebnis.
Wer stattdessen beim Kompilieren eine ausführbare a.out-Datei erzeugen möchte, muss dazu den zusätzlichen Parameter build
übergeben:
crystal build helloworld.cr
./helloworld
Die erzeugte Executable-Datei lässt sich auch unabhängig vom Crystal-Kompilierungssystem nutzen.
Eine vollumfängliche Besprechung der Crystal-Syntax würde den Umfang dieses Artikels sprengen, daher sollen im Folgenden nur die wichtigsten und interessantesten Aspekte in den Fokus rücken. Dazu zählen unter anderem Ranges, wie sie auch in Python vorkommen. Eine Range ist ein Variableninhalt, den der Compiler anhand der Benutzereingaben automatisch generiert und der das Hantieren mit for
-Schleifen oder großen Feldern von Konstanten überflüssig macht.
Crystal erlaubt Entwicklern das Erzeugen mehr oder weniger beliebiger Ranges. Zur Anschauung sollen eine Zahlen- und eine Buchstaben-Range genügen:
range1 = 1..10
range2 = 'a'..'z'
p range1, range2
Der Crystal-Compiler liefert das in Abbildung 4 gezeigte Ergebnis:
Eine Range zerlegt Crystal normalerweise nicht in ihre einzelnen Mitglieder, sondern sie gilt im Sinne der Mengenlehre als theoretische Beschreibung ihres Inhalts. Das lässt sich beispielsweise durch Ausführen einiger Methoden der Range bestätigen:
range1 = 1..10
range2 = 'a'..'z'
p range1.sum
range1.each do |laeufer|
p laeufer
end
Abbildung 5 zeigt das Ergebnis des Kompilierens: Die sum
-Operation addiert die Elemente, während each do
eine Art Iterator realisiert. Die sum
-Operation funktioniert nicht mit allen Range-Typen gleichermaßen. Wer die Zeichen-Range anwendet, erhält vom Compiler die Fehlermeldung "Error: undefined method 'zero' for Char.class".
An dieser Stelle kann man kurz überprüfen, ob Whitespaces für das Ausführen von Crystal-Programmen relevant sind. Hierzu genügt es, die Indentierung vor dem Statement zu entfernen:
range1.each do |laeufer|
p laeufer
end
Auch dieser Code lässt sich problemlos ausführen – Whitespaces spielen demnach keine Rolle. Crystal verhält sich bei der Indentierung offensichtlich genauso wie Visual Basic und vergleichbare Sprachen.
Kontrollstrukturen in Crystal
Das Vorhandensein von Ranges ist ein Indiz dafür, dass die Programmiersprache auch mit Selektionen und Iterationen umgehen können sollte.
Der folgende Code dient als Probelauf für Selektionen:
if range1.includes? 12
p "Inkludiert"
else
p "Inkludiert nicht"
end
unless range1.includes? 12
p "Unless matcht!"
end
Neben der an C erinnernden If-Else-Selektion findet sich hier auch ein unless
, ein "umgekehrtes If", das seine Payload immer dann ausführt, wenn eine Bedingung entweder false
oder nil
zurückliefert.
Da 12 offenkundig nicht Teil des Wertebereichs von 1 bis 10 ist, gibt das Programm nach dem Ausführen die Werte "Inkludiert nicht" und "Unless matcht!" zurück.
Die in den Selektionen zu verwendenden Auswahlkriterien sind bei Crystal flexibel. Für einen Vergleich auf Gleichheit sieht der Code folgendermaßen aus:
if 2 == 2
p "Inkludiert"
end
Selektionen lassen sich in Crystal auch zum Ermitteln von Ergebnissen nutzen, die zur weiteren Verwendung in eine Variable einfließen:
value = if 2 == 2
3
else
4
end
p value
Die allein in separaten Zeilen stehenden Zahlen 3 und 4 könnten auch als Strings oder andere Werte im Code Verwendung finden. Generell gilt jedoch, dass der durch p value
ausgegebene Wert der Variable value
den von der Selektion zurückgegebenen Wert enthält:
crystal helloworld.cr
3
Wer sich im Detail mit den verschiedenen Syntax-Elementen von Crystal beschäftigen möchte, findet weitere Informationen in der auf der Projektwebseite bereitstehenden Sprachbeschreibung [8].
Das aus C und vergleichbaren Sprachen bekannte – und für viele Fehler verantwortliche – Case-Statement findet sich auch in Crystal. Die bereits erwähnte Möglichkeit, mehrere Werte durch Kommata zusammenzufassen, lässt sich auch hier zur Kombination von Bedingungen heranziehen. Fallthrough hingegen unterstützt Crystal nicht:
case exp
when value1, value2
do_something
when value3
do_something_else
else
do_another_thing
end
Interessant ist in diesem Zusammenhang jedoch, dass Crystal auch Ranges in Bedingungen einbeziehen kann.
Der Einsatz von Schleifen in Crystal ist weitgehend mit der von C bekannten Vorgehensweise vergleichbar. Die while
-Schleife ist wie folgt deklariert:
while some_condition
do_this
end
Vergleichbar zur beschriebenen Vorgehensweise bei der if
-Selektion lässt sich auch while
zum Zurückgeben von Werten nutzen. Dazu dient das break
-Statement, das sich konditional aufrufen lässt und den an den Aufrufer zu retournierenden Wert festgelegt:
a = 0
x = while a < 5
a += 1
break "three" if a == 3
end
x # => "three"
Wichtig ist, dass eine durch false
-werden ihrer Bedingung endende while
-Schleife den Wert nil
zurückliefert:
x = while 1 > 2
break 3
end
x # => nil
Analog zu unless
gibt es mit until
auch eine invertierte Version der while
-Schleife, die so lange arbeitet, bis die übergebene Bedingung den Wert true
annimmt. Dabei ist until
explizit keine fußgesteuerte Schleife – eine solche ist in Crystal derzeit nicht implementiert:
until some_condition
do_this
end
Auch Zählschleifen mit for
sind im Sprachstandard von Crystal nicht vorgesehen.
Erzeugen einer simplen Webapplikation mit einem Framework
Crystal bringt ebenfalls eine Paket- und Projektverwaltung mit, die sich analog zu pip und Co. verhält. Eine kleine Webapplikation auf Basis des Frameworks Athena soll die Besonderheiten der Paketverwaltung von Crystal näher beleuchten.
Der im Crystal-Compiler enthaltene Projektverwalter erzeugt anhand der nachfolgenden Kommandos eine neue Projektstruktur:
tamhan@tamhan-VirtualBox:~$ mkdir crystalspace
tamhan@tamhan-VirtualBox:~$ cd crystalspace/
tamhan@tamhan-VirtualBox:~/crystalspace$ crystal init app tamswebapp
. . .
create /home/tamhan/crystalspace/tamswebapp/shard.yml
. . .
tamhan@tamhan-VirtualBox:~/crystalspace$ cd tamswebapp/
tamhan@tamhan-VirtualBox:~/crystalspace/tamswebapp$
Der Aufruf crystal init app
generiert ein neues Unterverzeichnis im aktuellen Arbeitsverzeichnis der Shell, in dem alle zum Projekt gehörenden Dateien unterkommen. Neben einer Readme- und verschiedenen Steuerungsdateien für das Git-Versionskontrollsystem ist vor allem die Datei /home/tamhan/crystalspace/tamswebapp/shard.yml relevant. Sie legt fest, welche der als Shard (englisch für (Ton-)Scherbe) bezeichneten Bibliotheken das vorliegende Projekt zum erfolgreichen Kompilieren benötigt.
Die Weiterentwicklung von Crystal-Modulen erfolgt im Allgemeinen auf GitHub. Das Athena-Modul und dessen jeweils aktuelle Version finden sich allerdings auf einer eigenen Website [9].
Sobald die Datei shard.yml zur Bearbeitung freigegeben ist, lässt sich die Athena-Bibliothek hinzufügen. Nach dem Öffnen der Konfigurationsdatei präsentiert sie unter anderem den targets
-Block. Er legt fest, welches Deliverable aus dem vorliegenden Crystal-Projekt zu generieren ist. Das eigentliche Einbinden des Athena-Frameworks erfolgt dann durch das Einfügen eines dependencies
-Blocks:
targets:
tamswebapp:
main: src/tamswebapp.cr
dependencies:
athena:
github: athena-framework/framework
version: ~> 0.17.0
crystal: 1.6.2
Während der version: ~> 0.17.0
-Operator die zu verwendende Version festlegt, ist die in Crystal enthaltene Paketverwaltung in der Lage, speziell formatierte GitHub-Repositories zu durchforsten und die in ihnen enthaltenen Inhalte direkt in den Kompilierungsprozess einzubinden.
Anders als Gradle lädt Crystal in der Regel nicht bei jedem Build-Prozess alle Bibliotheksressourcen herunter und vermeidet so unnötige Latenzen. Kehrseite dieser Optimierung ist jedoch, dass Entwicklerinnen und Entwickler das Herunterladen und Aktualisieren der diversen Dependencies gegebenenfalls manuell anstoßen müssen:
tamhan@tamhan-VirtualBox:~/crystalspace/tamswebapp$ shards install
Resolving dependencies
. . .
Das Kommandozeilenwerkzeug shards ist in der Lage, die in den verschiedenen Bibliotheken enthaltenen Unterabhängigkeiten zu berücksichtigen (s. Abb. 6). Das Tool ist jedoch nicht Teil der Hauptdistribution von Crystal.
Die Umsetzung der Webapplikation erfolgt bei Nutzung des Athena-Frameworks im MVC-Pattern (Model, View, Controller). Crystal bringt darüber hinaus eine durchaus vollständige Implementierung der diversen objektorientierten Designparadigmata mit. Damit sind auch komplexe Web-Anwendungen umsetzbar. Wichtig für den Anfang ist, dass der Einsprungspunkt in die Webapplikation immer in Form einer Athena:: Framework:: Controller
-Klasse vorliegen muss. Die Applikationslogik muss von dieser "erben" und der neu erzeugten Klasse – wie etwa aus JavaScript bekannt – die verschiedenen, für die Verarbeitung der eingehenden URLs notwendigen Event Handler einschreiben.
Zum Ableiten muss das Programm allerdings einen Einsprungspunkt erhalten. Eine Analyse der Build-Steuerungsdatei informiert darüber, dass der Hauptteil des Codes in der folgenden Datei zu finden ist:
tamhan@tamhan-VirtualBox:~/crystalspace/tamswebapp/src$ code tamswebapp.cr
In Visual Studio Code präsentiert sich nach dem Öffnen der folgende Korpus:
# TODO: Write documentation for `Tamswebapp`
module Tamswebapp
VERSION = "0.1.0"
# TODO: Put your code here
end
Module übernehmen in Crystal dabei die Rolle der unter anderem aus C# bekannten Namespaces. Um den in anderen Programmiersprachen erforderlichen Einsprungspunkt (Methode Main) zu simulieren, lässt sich mit dem folgenden Code im Modul-Korpus ein aktiver Aufruf unterbringen:
require "athena"
# TODO: Write documentation for `Tamswebapp`
module Tamswebapp
VERSION = "0.1.0"
# TODO: Put your code here
p "Hallo Welt"
end
Neben einer Begrüßungsmeldung taucht in der Ausgabe auch erstmals das Statement Require
auf. Es legt fest, dass das vorliegende Produkt von der Athena-Bibliothek abhängig ist.
Im nächsten Schritt lässt sich der Code mit Crystal Run kompilieren:
tamhan@tamhan-VirtualBox:~/crystalspace/tamswebapp$ crystal run src/tamswebapp.cr
"Hallo Welt"
Ungewöhnlich ist am vorliegenden Code lediglich, dass der Aufruf nicht unter Verwendung der Spezifikationsdatei, sondern der Codedatei erfolgt. Die Ausgabe des Begrüßungsstrings informiert darüber, dass das Ausführen des Einsprungspunkts erfolgreich verlief.
Wie bei JavaScript gilt auch im Fall von Crystal, dass auf modulglobaler Ebene angelegter Code im Rahmen der Initialisierung beziehungsweise der Bereitstellung des Moduls zur Ausführung gelangt.
Das Erzeugen der Zugriffslogik beginnt mit:
module Tamswebapp
VERSION = "0.1.0"
class ExampleController < ATH::Controller
@[ARTA::Get("/")]
def index : String
"Hello World"
end
end
Das Symbol <
informiert Crystal über die anzulegende Vererbungsbeziehung.
Für das Festlegen der Route kommt eine Action zum Einsatz, die wie im vorangehenden Listing gezeigt angelegt wird. Der mit def
beginnende Teil deklariert eine gewöhnliche Methode, die den Namen index
aufweist, keine Parameter entgegennimmt und den Aufrufer mit einem String ausstattet. Interessant ist hier die Art des Zurückgebens des Strings: Crystal nutzt kein Statement wie return
, sondern es reicht aus, den Rückgabewert allein in einer Zeile stehen zu lassen.
Anstelle des vorher als Instrumentierung dienenden Print-Aufrufs ist nun ATH.run
aufzurufen. Die Methode durchsucht die gesamte Solution nach als Route markierten Methoden und bringt diese danach selbsttätig zur Ausführung:
ATH.run
end
Nach dem Speichern der .cr-Datei lässt sich die Kompilierung anstoßen:
tamhan@tamhan-VirtualBox:~/crystalspace/tamswebapp$ crystal run src/tamswebapp.cr
/usr/bin/ld: cannot find -lpcre2-8 (this usually means you need to install the development package for libpcre2-8): No such file or directory
crystal run
lässt sich an dieser Stelle etwas Zeit, um anschließend die gezeigte Fehlermeldung zu retournieren.
Ursache dafür ist, dass Frameworks in der Welt von Crystal eigene Dependencies zu weiteren Binärdateien deklarieren dürfen. Leider löst shard
diese im Rahmen der Bereitstellung des Moduls nicht selbsttätig auf. Ubuntu informiert an dieser Stelle allerdings durch Ausgabe einer Fehlermeldung darüber, dass zum Ausführen des Programms die PCRE-Bibliothek erforderlich ist.
Dieses Problem lässt sich durch Einsatz von apt-get
beheben:
tamhan@tamhan-VirtualBox:~/crystalspace/tamswebapp$ sudo apt-get install libpcre2-dev
Nach dem erfolgreichen Herunterladen der Bibliothek lässt sich ein erneuter Ausführungsversuch wagen:
tamhan@tamhan-VirtualBox:~/crystalspace/tamswebapp$ crystal run src/tamswebapp.cr
2022-11-08T04:17:14.363019Z INFO - athena.framework: Server has started and
is listening at http://0.0.0.0:3000
Da Ports kleiner als 1024 unter Linux privilegiert sind, entscheidet sich das Athena-Framework eigenständig für einen anderen Port.
Beim Testen der Applikation erweist sich der Browser Firefox mitunter als etwas widerspenstig, weil er bei Eingabe von localhost:3000/ automatisch den HTTPS-Präfix angefügt (s. Abb. 7).
Nach dem Editieren der Adresszeile ergibt sich das in Abbildung 8 gezeigte Ergebnis. Es informiert darüber, dass Firefox die Informationen erfolgreich aus der Applikation entnehmen konnte.
Crystal ist innovativ, bleibt aber den etablierten Konventionen verpflichtet
Die ersten Experimente mit Crystal zeigen, dass die objektorientierte Programmiersprache trotz diverser innovativer Erweiterungen nicht übermäßig mit den etablierten Konventionen bricht. Sie spielt aber beispielsweise mit ihrer Möglichkeit, Applikationen direkt in Maschinencode zu kompilieren, Vorteile gegenüber Sprachen wie Java aus. Auf die umfassende Flexibilität wie sie zur Laufzeit interpretierte Sprachen bieten, müssen Crystal-Entwicklerinnen und -Entwickler hingegen verzichten. Dennoch punktet die Sprache auch gegenüber Präprozessor-Lösungen, die bei den mit C verwandten Programmiersprachen weit verbreitet sind.
In einem weiteren zu Crystal geplanten Folgeartikel soll die Interaktion mit C-Code näher beleuchtet und ein Ausblick auf die Metaprogrammierung mit selbst modifiziertem beziehungsweise selbst generiertem Code gegeben werden.
Tam Hanna ist Autor, Trainer und Berater mit den Schwerpunkten Webentwicklung und Webtechnologien. Er lebt in Ungarn.
(map [10])
URL dieses Artikels:
https://www.heise.de/-7454848
Links in diesem Artikel:
[1] https://www.heise.de/news/Visual-Basic-6-0-noch-weit-verbreitet-199799.html
[2] https://www.heise.de/news/Programmiersprache-Crystal-erfolgreich-auf-Version-1-0-gezuechtet-5995646.html
[3] https://github.com/crystal-lang/crystal
[4] https://crystal-lang.org/install/
[5] https://crystal-lang.org/install/on_ubuntu/
[6] https://github.com/elbywan/crystalline#pre-built-binaries
[7] https://crystal-lang.org/
[8] https://crystal-lang.org/reference/1.6/
[9] https://athenaframework.org/getting_started/
[10] mailto:map@ix.de
Copyright © 2023 Heise Medien