en
de

Elixir – Der funktionale Zaubersaft für IoT

15 April 2015
| |
Lesezeit: 7 Minutes

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.

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, elegante Konzepte ins Extreme ausgeweitet werden, um dann in einem gesund geschrumpften 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 Gebilde 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.

def module Hello do
  def say_hello() do
    IO.puts "Hello World!"
  end
end

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 bekommmen (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. :ok)
  • Listen (z.B. [1, 2, 3]) sind die universelle Datenstruktur, Tupel kombinieren Daten (z.B. {:ok, 28}), 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.

def factorial(0) do
  1
end
def factorial(n) do
  n * factorial(n-1)
end

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 {:ping, origin} ein :pong an den Sender origin schickt, beim Empfang von :stop termininiert und :ok zurückliefert. Dank Tail-Call-Optimization in der Erlang-VM läuft diese rekursive Funktion Jahre lang, ohne dass zu einer Stack-Explosion kommt.

def ping_server() do
  receive do
    {:ping, origin} ->
      send(origin, :pong)
      ping_server()
    :stop -> :ok
  end
end

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 Enumerable 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 Enum-Module nutzbar, da diese nur voraussetzen, dass die Funktionen aus Enumerable verfügbar.

iex> liste = [1, 2, 3, 4, 5]
iex> m = Enum.map(liste, fn(x) -> x*2 end)
[2, 4, 6, 8, 10]

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 |>, die Funktionskombination mit einer suggestiven Syntax unterstützt. Der Pipe-Operator injiziert sein linkes Argument als erstes Argument für den rechtsstehenden Ausdruck. Ein einfaches Beispiel für das obige macht den Unterschied deutlich:

iex> liste = [1, 2, 3, 4, 5]
iex> m = liste |> Enum.map(fn(x) -> x*2 end)
[2, 4, 6, 8, 10]

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

iex> double = fn(x) -> x*2 end
iex> m = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |> 
    Enum.map(double) |> Enum.take(5)
[2, 4, 6, 8, 10]

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 Laufzeit-Impact, 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 iex.

Elixir bringt sein eigenes Build-System mix mit. mix 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 mix in Elixir geschrieben ist und eine offene API hat, sind Erweiterungen leicht programmiert. Eine fundamentale mix-Erweiterung ist der Package-Manager hex.pm, 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 ExUnit. 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 iex 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 Dialyzer aus dem Erlang-Toolset nutzt diese Informationen, um nach Fehlern zu suchen. Das Typsystem lässt keinen echten statischen Typecheck zu, aber wenn der Dialyzer 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 ecto eine Bibliothek für den Datenbankzugriff. Zur Formulierung von Queries nutzt ecto 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

Kommentare (0)

×

Updates

Schreiben Sie sich jetzt ein für unsere zwei-wöchentlichen Updates per E-Mail.

This field is required
This field is required
This field is required

Mich interessiert

Select at least one category
You were signed up successfully.

Erhalten Sie regelmäßige Updates zu neuen Blogartikeln

Jetzt anmelden

Oder möchten Sie eine Projektanfrage mit uns besprechen? Kontakt aufnehmen »