Elixir – Der funktionale Zaubersaft für IoT

Wer Software-Lösungen für Investitions- und Gebrauchsgüter-Hersteller entwickelt, kommt derzeit am Thema Internet-of-Things, kurz: IoT, nicht vorbei. Dabei stellt sich bei jedem neuen Projekt die Frage nach der geeigneten Programmiersprache.

» Von Klaus Alfert für Zühlke, 16.04.2015 09:00.

weitere Artikel

Zühlke Blog


Dies ist ein Blog-Eintrag von: zuehlke.ch

Join the Conversation

Da sich Programmiersprachen typischerweise entlang der ihnen zugedachten Aufgaben weiterentwickeln, entsteht derzeit im Rahmen neuer Anforderungen von IoT mit Elixir eine Sprache, die das Zeug dazu hat, sich zu einem Industriestandard zu entwickeln. Warum das so ist, und was es zu Elixir zu wissen gibt, und was das Ganze mit Whatsapp zu tun hat, wird dieser Beitrag näher beleuchten.

Evolution von Programmiersprachen

Die Evolution von Programmiersprachen zeigt immer wieder, dass einfache, elegantze Konzepte ins Extreme ausgeweitet werden, um dann in einem gesund geschrumpftenm Maß die richtige Produktivität zu entwickeln.

So ist in den 60er Jahren aus Algol60 das Monster PL/1 geworden, erst Pascal und C in den 70er haben dann den passenden Umfang imperativer Sprachen definiert. In den 80er Jahren ist aus dem puristischen Smalltalk-80 mit C++ ein immer komplexeres Gebildes entstanden. 10 Jahre später wurde mit Java und C# in den 90er und 2000ern die OO-Welt beherrschbar. Und im funktionalen? Aus den eleganten Sprachen Haskell, ML, und Lisp hat sich das mächtige Scala gebildet, aber noch fehlt der Reduzierungsschritt.

Mit Elixir zeigt sich nun eine funktionale Sprache, die ein geschickt ausgewogenes Verhältnis zwischen Komplexität, Ausdrucksstärke und Pragmatismus bietet. Elixir ist somit ein Kandidat, um funktionale Sprachen breit nutzbar zu machen und so in den Mainstream zu bringen.

Elixir: Lessons Learned from the Past

Elixir ist von José Valim entwickelt worden. José ist Core-Entwickler für Ruby on Rails gewesen und hatte die Aufgabe, Rails und Ruby multithreading-fähig zu machen. Ihm ist klar geworden, dass dies ein ziemlich hoffnungsloses Unterfangen ist. Bei der Suche nach Alternativen ist er auf Erlang gestoßen, einer funktionalen Programmiersprache von Ericsson, die aggressiv Concurrency einsetzt, um eine hohe Zuverlässigkeit zu erreichen und gleichzeitig hervorragende Skalierbarkeit in Multicore- und in verteilten Systemen zu bieten.

Erlang selbst ist als Sprache etwas eigenwillig in seiner Syntax und konservativ in seinen Features. Aber das entscheidendere ist das Ökosystem. Die Erlang-VM, die Basisbibliotheken, die Erlang/OTP Middleware: All dies ist seit über 20 Jahren im Industrieeinsatz kampferprobt und dabei so konsequent auf Zuverlässigkeit, Skalierbarkeit, Parallelität und Concurrency ausgelegt, dass auch die Macher von Whatsapp ihren für „no- downtime“ bekannten Messenger-Service damit laufen lassen. Was nun noch fehlt, ist die richtige Entwicklerproduktivität. Dafür entwickelte José dann die neue Sprache Elixir mit der Erlang-VM als Zielumgebung.

Elixir lehnt sich optisch stark an Ruby an, so dass man im ersten Moment geneigt ist, es als Ruby für die Erlang-VM zu bezeichnen. Doch das ist zu kurz gegriffen, Elixir bietet ganz anderes: Es kombiniert geschickt die guten Eigenschaften von vielen anderen Sprachen, ohne das ganze Ganze zu komplex und überbordend zu gestalten.

Um mit Elixir herumzuspielen, kann man nach der Installation die interaktive Elixir-Shell iex starten, um im Interpreter Elixir-Anweisungen auszuprobieren. Wenn im folgenden die Programmbeispiele mit iex> beginnen, dann sind dies Beispielein- und ausgaben in iex.

Elixir Features

Elixir ist eine dynamisch getypte, funktionale Sprache. Sie benötigt einen Compiler, um den Bytecode für den Erlang VM zu erzeugen. In Elixir gibt es keine Objekte und keine globalen Variablen. Der gesamte Code besteht aus Funktionen, die in Modulen gebündelt werden. Da die Erlang-VM das Zielsystem ist, sind die Kerneigenschaften zu Erlang daher gleich:

  • kein statisches Typsystem, wohl aber Typprüfungen zur Laufzeit
  • Variablen sind immutable und können nur einmal einen Wert zugewiesen bekommmenbekommen (auch wenn Elixir scheinbar Mehrfachzuweisungen erlaubt)
  • der Garbage Collector kümmert sich um die Speicherverwaltung
  • Neben Zahlen und Strings existieren noch Symbole als einfache Werte. Sie werden mit einem Doppelpunkt als Präfix geschrieben (z.B. <tt>:ok</tt>)
  • Listen (z.B. <tt>[1, 2, 3]</tt>) sind die universelle Datenstruktur, Tupel kombinieren Daten (z.B. <tt>{:ok, 28}</tt>), Maps bilden universelle Key-Value-Paare.
  • Funktionen sind ebenfalls Werte und können als Parameter oder Ergebnisse von anderen Funktionen dienen (higher order functions)

Pattern Matching

Elixir nutzt Pattern Matching, um Funktionen elegant formulieren zu können. Pattern Matching bedeutet, dass es je nach Wertebelegung der Funktionsparameter unterschiedliche Methodenrümpfe geben kann. Dabei können die Wertebelegungen beliebig komplex werden und Wildcards beinhalten. Dieses sehr mächtige Werkzeug reduziert die Komplexität von Funktionen erheblich, so dass nur selten if-Anweisungen benötigt werden.

Prozesse

Wie in Erlang bietet Elixir Prozesse an. Dies sind nebenläufige Funktionen innerhalb der VM, die nicht(!) auf Betriebssystem-Threads abgebildet werden. Die Erlang-VM ist darauf optimiert mit sehr vielen Prozessen umzugehen, auf einem Notebook sind eine Million gleichzeitig existierender Prozesse durchaus möglich und beherrschbar.

Elixir-Prozesse haben eine Mailbox, so dass man ihnen Nachrichten schicken kann. Dies erfolgt immer asynchron, es wird nicht auf eine Reaktion des Empfängers gewartet. Die Nachrichten selbst können beliebig komplex sein, sind aber immer unveränderbar. Alle anderen Variablen innerhalb eines Prozesses sind lokale Variablen der Prozessfunktion und so von außen nicht zugreifbar. Damit ist die erste große Fehlerquelle der nebenläufigen Programmierung ausgeschaltet: es gibt zwischen zwei Prozessen keinen geteilten Zustand, den beiden Prozesse verändern können!

Pattern Matching wird auch in Prozessen verwendet, um die verschiedenen Nachrichten, die ein Prozess empfangen kann, zu unterscheiden. Ein einfacher Ping-Server ist daher eine rekursive Funktion, die auf die Nachricht <tt>{:ping, origin}</tt> ein <tt>:pong</tt> an den Sender <tt>origin</tt> schickt, beim Empfang von <tt>:stop</tt> termininiert und <tt>:ok</tt> zurückliefert. Dank Tail-Call-Optimization in der Erlang-VM läuft diese rekursive Funktion Jahre lang, ohne dass zu einer Stack-Explosion kommt.

Protokolle und der Pipe-Operator

Interfaces werden in Elixir als Protocols definiert, für die es verschiedene Implementierungen geben kann. Dies entspricht im Wesentlichen Type Classes aus Haskell oder Traits aus Scala. In Elixir wird in der Standardbibliothek das Protocol <tt>Enumerable</tt> definiert, um eine Datenstruktur zu traversieren. Für jede beliebige Implementierung (z.B. Listen, Bäume, Hashmaps, …) sind daher alle Higher Order Functions aus dem <tt>Enum</tt>-Module nutzbar, da diese nur voraussetzen, dass die Funktionen aus <tt>Enumerable</tt> verfügbar.

Funktionale Programmierung besteht in seinem Wesen aus Transformationen von Eingabewerte in Ausgabewerte. Ein Programm is somit eine Kombination von Funktionen, die genau diese Transformationen durchführen. In Elixir gibt es den Pipe-Operator <tt>|></tt>, die Funktionskombination mit einer suggestiven Syntax unterstützt. Der Pipe-Operator injeziert sein linkes Argument als erstes Argument für den rechtsstehenden Ausdruck. Ein einfaches Beispiel für das obige macht den Unterschied deutlich:

Richtig interessant wird der Pipe-Operator, wenn man ihn mehrfach anwendet und so eine Pipeline erzeugt:

Dies ist eine sehr lesbare Alternative zu geschachtelten Funktionen oder mehreren Zwischenvariablen, die die Zwischenergebnisse aufnehmen.

Meta-Programmierung mit Makros

Während in C-artigen Sprachen (inklusive Erlang) Makros durch einen Präprozesser im Quelltext arbeiten, in Java und Scala dergleichen gar nicht erst zu finden ist, folgen Makros in Elixir dem Ansatz von Lisp: Ein Makro ist eine Elixir-Funktion, die vom Compiler während der Compilerlaufes aufgerufen wird, um den Syntaxbaum des Compilers zu manipulieren. Damit kann man eigene Spracherweiterungen, neue Schlüsselworter oder interne DSLs entwickeln. Im Gegensatz zur Meta-Programmierung von Ruby haben Elixir-Makros keinen Laufzeitimpact, die sie vollständig zur Compile-Zeit berechnet werden.

Makros kommen in Elixir oft zum Einsatz, da sie es erlauben, die Sprache ausgehend von einem kleinen Kern sukzessive zu erweitern, ohne den Compiler verändern zu müssen. So ist der Pipe-Operator als Makro implementiert, ebenso die Assertions in der Testbibliothek ExUnit. Macros sind eine elegante Möglichkeit, um dem Entwickler Syntactic Sugar anzubieten.

Das Tooling im Kleinen hilft sehr

Auch im vermeintlich Kleinen bietet Elixir eine Menge Goodies, angefangen mit der oben erwähnten interaktiven Elixir-Shell <tt>iex</tt>.

Elixir bringt sein eigenes Build-System <tt>mix</tt> mit. <tt>mix</tt> ruft nicht nur den Compiler auf, sondern bietet Dependency-Management, die Ausführung von Test-Suiten, die Generierung von API-Docs, den Build von Erlang-Modulen und mehr. Da <tt>mix</tt> in Elixir geschrieben ist und eine offene API hat, sind Erweiterungen leicht programmiert. Eine fundamentale <tt>mix</tt>-Erweiterung ist der Package-Manager <tt>hex.pm</tt>, der, ähnlich wie bei Ruby Gems und in Maven, Elixir-Pakete mit Versionen und Abhängigkeiten verwaltet und zentral deployt.

Zur Standardbibliothek gehört bei Elixir auch das Testframework <tt>ExUnit</tt>. Es bietet alle üblichen Funktionen, führt die Tests wenn möglich parallel aus und generiert auf Wunsch Coverage-Informationen. Die Assertion-DSL arbeitet mit Makros und kann so sehr präzise Informationen über erwartete und tatsächliche Werte im Fehlerfall generieren, ohne dass man als Entwickler eingreifen muss. Endlich braucht man keine Debugger mehr, um zu sehen, welchen Unterschied aktuelle und erwartete Werte in der Assertion haben.

In Elixir werden API-Kommentare mit Markdown-Syntax geschrieben. Der Aufruf von mix docs generiert die Web-Seiten dazu. Die Kommentare sind – wie in Python – first class citizens und daher auch zur Laufzeit abrufbar. So kann man in <tt>iex</tt> die Kommmentare als Onlinehilfe zu einem Modul abrufen. Die Testumgebung durchsucht dagegen die Kommentare nach Testfällen bzw. Beispielaufrufen und interpretiert diese als Unit-Tests. So stimmen API-Doc und Implementierung auch nachweislich überein!

Zwar erfordert Elixir keine Datentypendefinition, aber man kann Typspezifikationen als zusätzliche Angaben für Funktionen schreiben. Der Elixir-Compiler ignoriert diese Informationen, aber der optimistische Typechecker <tt>Dialyzer</tt> aus dem Erlang-Toolset nutzt diese Informationen, um nach Fehlern zu suchen. Das Typsystem lässt keinen echten statischen Typecheck zu, aber wenn der <tt>Dialyzer</tt> einen Fehler findet, dann ist dort bestimmt auch ein Fehler zur Laufzeit. So bekommt man von beiden Seiten das Beste: ein dynamisches Typsystem, das nicht zu einem zu engen Korsett wird, zusammen mit einem statischen Typechecker, der soviele Fehler wie möglich findet.

Im direkten Umfeld von Elixir findet sich mit <tt>ecto</tt> eine Bibliothek für den Datenbankzugriff. Zur Formulierung von Queries nutzt <tt>ecto</tt> eine von LINQ inspirierte Syntax. Auch hier zeigen die Elixir-Makros ihre ganze Ausdrucksstärke.

Und der Sweet-Spot? Hohe Last und Zuverlässigkeit!

Für was kann man nun Elixir nutzen? Elixir benötigt die Erlang-VM als Laufzeitumgebung und erbt auf diese Weise eine Menge Eigenschaften. Elixir punktet bei der Entwicklung von Server-Systemen, die mit einer hohen Zahl von gleichzeitig laufenden Sessions umgehen können müssen. Die Erlang/OTP Bibliotheken erlauben auf einfache Weise enorm zuverlässige Systeme zu bauen, wie sie sonst nur mit sehr viel größeren Aufwand erreicht werden.

Dank Prozessen, Pattern Matching und ausgefeilten Kommunikationsbibliotheken ist die Implementierung neuer Kommunikationsprotokolle in Elixir überraschend einfach. Hier profitiert Elixir von den Konzepten und Erfahrungen der Erlang-Macher, die Erlang zur Entwicklung von Internet-Backbone-Systemen konzipiert und eingesetzt haben (und tun!).

Braucht man Server-Systeme, die z.B. eine Vielzahl von Geräten im IoT-Umfeld anbinden müssen, so ist Elixir eine hervorragende Wahl.

Fazit

Programmieren in Elixir macht Spaß und ist sehr produktiv. Es ist die wohlüberlegte Kombination sinnvoller Features, die Elixir attraktiv macht. Das Sprachdesign ist keine wissenschaftliche Meisterleistung, aber dafür voller Pragmatismus auf der Basis ausgereifter Konzepte. Die wachsende Community zeigt, dass José Valim den richtigen Weg einschlägt. Elixir hat seine Chance verdient und ich glaube, dass Elixir sie auch nehmen wird.

Links

Werbung

KOMMENTARE

Keine Kommentare

KOMMENTAR SCHREIBEN

*
*
*
*

Alles Pflichfelder, E-Mail-Adresse wird nicht angezeigt.

Die Redaktion hält sich vor, unangebrachte, rassistische oder ehrverletzende Kommentare zu löschen.
Die Verfasser von Leserkommentaren gewähren der NMGZ AG das unentgeltliche, zeitlich und räumlich unbegrenzte Recht, ihre Leserkommentare ganz oder teilweise auf dem Portal zu verwenden. Eingeschlossen ist zusätzlich das Recht, die Texte in andere Publikationsorgane, Medien oder Bücher zu übernehmen und zur Archivierung abzuspeichern.