Vergleicht man die Geschichte von Erlang mit der von anderen Sprachen, so ist diese sicherlich keine die man besonders laut erzählen würde. Nicht weil man sich dafür schämt, im Gegenteil. Erlang hat einen vorbildlichen und erfolgreichen Weg hinter sich. Die Sprache ist jedoch aufgrund mangelnder Popularität funktionaler Programmiersprachen, nebst sämtlichen Hypes rund um die Objektorientierung, meist abseits des Rampenlichts gestanden. Wendet man jedoch den Blick nach vorne und schaut sich aktuelle Trends in der IT an, so sieht man den Anstieg der Beliebtheit funktionaler Sprachen.

Vergleichen wir unterschiedliche Sprachen in der Software-Entwicklung, so gibt es meist Bereiche in denen wir mit der jeweiligen Sprache weniger gut unterstützt werden. Denkt man an den Schritt von C zu Java, so fällt einem die Erleichterung der automatischen Speicherverwaltung ein. Natürlich gibt es weitere Vor- und jede Menge Nachteile in diesem Vergleich und man könnte ganze Bücher damit füllen. Doch denken wir hier zunächst weiter: Als Entwickler fühlen wir uns also oft sicherer bei der Entwicklung mit Java. Wir sind nicht gerade sparsam mit Ressourcen aber wir machen uns auch wenig sorgen um Memory Leaks. Wo also bewegen wir uns auf dünnem Eis? In der Parallelisierung, im Multithreading bzw. in der horizontalen Skalierung! Mittlerweile gibt es in Java viele Frameworks und Sprachneuerungen die uns die Arbeit in diesen Bereichen erleichtern sollen. Und es geht auch. Dennoch könnte man sagen, dass die horizontale Skalierung stets die Achillesferse des Java Ökosystems war. Vielleicht wäre es auch nicht falsch zu denken, dass die Parallelisierung das für Java ist, was die Speicherverwaltung für C bedeutet. Wo finden wir als Entwickler also das richtige Werkzeug für die teilweise sehr komplexen Probleme rund um das Thema parallele Programmierung? Wo also ist parallele Programmierung das für eine Sprache, was die Speicherverwaltung für Java ist: Ein fester Bestandteil der Plattform, ein durchgängiges Konzept, welches von Anfang an konsequent adressiert wurde. Die Antwort ist Erlang.

Das Erlang Ökosystem

Das Ökosystem um Erlang herum befindet sich in der Renaissance. Es ist über die Jahre gereift aber nicht veraltet. Die Größe ist überschaubar und der Ruf in der IT Community ist tendenziell gut. Auf Fachkonferenzen oder in Artikeln vernimmt man ab und zu ein schmunzeln: „Die Sprache ist zu technisch, zu grob“ aber es dominiert Respekt und Anerkennung bezüglich Stabilität, Robustheit in den schweren Disziplinen der massiven parallelen Verarbeitung. Harte Fakten wie erreichte 99,9999999% Verfügbarkeit über 20 Jahre kann die viel stärker polarisierende junge Plattform Node.js nicht vorweisen, die derzeit in ähnlichen Disziplinen ihre Daseinsberechtigung sucht.

Woraus besteht denn nun das Erlang Ökosystem? Natürlich aus der Sprache selbst. Diese ist nicht ganz so einfach zu klassifizieren: Sie ist funktional aber nicht rein funktional. Im Gegensatz zu Haskell sind Seiteneffekte möglich. Sie ist nicht objektorientiert, dennoch sind OO-Konzepte wie Polymorphismus, Abstraktion oder Objekte in Form von Closures vorzufinden. Erlang ist dynamisch getypt und eine Sprache der höheren Ordnung. Neben der Sprache besteht die Plattform aus zwei weiteren sehr wichtigen Komponenten, die den ein oder anderen Leser wohlmöglich positiv überraschen werden: Die Erlang Virtual Machine und der Garabage Collector. Erlang Code läuft auf einer VM. Dies bringt die bekannte und populäre Plattformunabhängigkeit mit sich, sowie die Fähigkeit weitere Sprachen, wie die jüngste Kreation namens „Elixir“, unterstützen zu können. Der Garbage Collector kümmert sich um die Freigabe von Speicher. Analog zur Java Plattform müssen sich Entwickler auch bei Erlang wenig um die Allokation und Freigabe von Speicher kümmern. Darüber hinaus ist Erlang vollständig Open Source und kann in GitHub eingesehen werden. [1] Die Erlang Public Lincense (EPL) steht dem freien oder kommerziellen Einsatz nicht im Weg. Den REPL Freunden wird die Erlang Shell zusätzlich an die Hand gegeben.

Im professionellen Einsatz wird Erlang stets in Verbindung mit dem hauseigenen Satz an Bibliotheken, welche u. A. auch als Middleware fungieren, verwendet. Der Überbegriff ist Open Telecom Platform (OTP), sollte an dieser Stelle andere Branchen allerdings nicht abschrecken. Der Name stammt aus dem ursprünglichen Anwendungsfall, nämlich einem der ersten Anwendungsfälle bei dem massive und stabile Echt-Zeit Parallelverarbeitung eine Rolle spielte: Dem gleichzeitigen Schalten zahlloser Telefongespräche. Mit der OTP finden die hinter Erlang stehenden Konzepte wie das „Let it crash“ Prinzip ihre Anwendung. Hierfür existieren sog. Behaviours, welche die Erlang Prozesse klassifizieren und Callbacks für die individuelle Ausgestaltung bereitstellen. Besonders populär ist der Supervisor, welche abstürzende Prozesse wieder zum Leben erweckt. (Siehe Abbildung 1)

Abb 1: Let it crash

Einsatzgebiete

Ob Web-Services via REST oder SOAP, Sockets, direkte Integration mit Java oder unterschiedlichen Datenbanken wie der eigenen Mnesia oder modernen NoSQL Datenbanken: Erlang ist in heterogenen Systemlandschaften zu hause. Dabei liegt der Fokus klar auf technischen Anforderungen: Cloud Server, Web Server, Chat-Systeme, Online Spiele Backends, eingebettete Systeme oder Analysen sind beispielhaft zu nennen. Bemerkenswert ist an dieser Stelle, dass die genannten Anwendungsfälle zu Zeiten der Entstehung Erlangs größtenteils unbekannt waren. Dies wirft den Gedanken auf, dass Erlang seiner Zeit voraus war. Anwendungen mit fachlichen oder graphischen Schwerpunkten gehören weniger zu den Einsatzgebieten Erlangs. Dies reflektiert auch ein Blick in die zur Verfügung stehenden Bibliotheken oder in aktuelle Anwendungen welche mit Erlang realisiert wurden wie beispielsweise WhatsApp, RabbitMQ, Riak oder CouchDB.

Werkzeuge

Ob Emacs, Sublime, Eclipse oder IntelliJ, es existieren ausgereifte Integrationen in den gängigen IDEs. Erlang selbst verfügt über ein integriertes Dependency Management und eine Test Unterstützung.

Erlangs Brillianten

Stabilität – Concurrency ist ein grundlegendes Konzept von Erlang. Anstelle von Threads, welche den Speicher gemeinsam nutzen, gibt es in Erlang leichtgewichtige Prozesse, die mit eigenem Heap und Stack arbeiten. Diese Prozesse können sich somit nicht gegenseitig interferieren und kommunizieren asynchron über Message Passing. Das hat den Vorteil, dass nachdem eine Message an einen Prozess versandt wurde, die Verarbeitung fortgesetzt werden kann (non-blocking). Jeder Prozess hat zudem noch so etwas wie einen Briefkasten, aus dem er die empfangenen Messages beliebig verarbeiten kann.

Fehlertoleranz (Let it crash) – Ein sog. Supervisor hat die Aufgabe Prozesse zu überwachen. Der Programmierer muss dadurch nicht mehr defensiv entwickeln und kann zunächst von einem idealen Szenario ausgehen. Somit wird dem Programm in Fehlerfällen bewusst die Freiheit gelassen abzustürzen. Ein Supervisor Prozess greift dann rettend ein und kann den abgestürzten Prozess neustarten. In Erlang wird also nicht das nahezu unmögliche versucht und an alle Fehler, die auftreten können zu denken, sondern es wird akzeptiert, dass dies gar nicht möglich ist. Aber wenn etwas Schlimmes passiert, dann weiß Erlang damit zugehen.

Verfügbarkeit – Da in Erlang Module zur Laufzeit neu geladen werden können, ist es möglich auf Deployment bedingte Downtime zu verzichten. Nicht nur bei Updates von bestehenden, sondern auch das Installieren von neuen Modulen kann dynamisch zur Laufzeit geschehen. (Hot Swapping) Was in anderen Ökosystemen mit kommerziellen Produkten teuer dazu gekauft werden kann oder mit viel Aufwand und Frameworkeinsatz künstlich erzeugt werden muss, ist für Erlang das normalste der Welt. So erreichte Erlang unschlagbar hohe Verfügbarkeitskennzahlen von 99,9999999%.

Ready-to-use Komponenten – Erlang strukturiert seine Bibliotheken in Behaviours. Dazu gehören unter anderem Worker Behaviours wie zum Beispiel Event Handler, endliche Zustandsautomaten (finite state machine) und Server. Die OTP Behaviour lassen sich auch als eine Sammlung von ausgereiften Lösungen für Concurrency Design Patterns verstehen.

Die Sprache Erlang

Im folgenden Abschnitt möchten wir die Sprache Erlang etwas genauer vorstellen. Zunächst hilft das Ping Pong Beispiel in Listing 1, welches einen idealtypischen Aufbau einer Erlang Datei demonstriert, sowie das Message Passing zwischen Erlang Prozessen veranschaulicht.

-module(pingpong).
-export([ping/1,pong/0,start/0]).

ping(PongPID) ->
    % self() liefert die eigene Process ID (PID)
    PongPID ! {ping, self()}, % ping Message an pong senden
    receive
        pong -> % pong Message empfangen
            io:format("pong (~p)~n", [PongPID])
    end,
    ping(PongPID).

pong() ->
    receive
        {ping, PingPID} -> % ping Message empfangen
            io:format("ping (~p)~n", [PingPID]),
            PingPID ! pong % pong Message an ping senden
    end,
    pong().

start() ->
    % Die Prozesse ping und pong asynchron starten.
    PongPID = spawn(pingpong, pong, []),
    % Die ping Funktion bekommt noch die Process ID des pong Prozesses.
    spawn(pingpong, ping, [PongPID]).
Listing 1: Anatomie einer ERL-Datei

Tupel, Listen – Eine Möglichkeit zur Strukturierung von Daten bilden Tupel. Tupel werden in geschweiften Klammern definiert. Die einzelnen Elemente werden durch ein Komma getrennt. Es können auch Tupel innerhalb von Tupel geschachtelt werden. Eine Konvention ist es Tupel mit dem ersten Element zu taggen damit es identifizierbar wird. Beispielsweise statt {1,2}. besser {point, {1,2}}.. Die Notation einer Liste in Erlang erfolgt in eckigen Klammern. Auch hier können verschiedene Werte als Elemente einer Liste verwendet werden [1,1.234,{point,1,2},[3,2,1]].. Strings werden in Erlang auch als Listen dargestellt. Beispielsweise ergibt [65,66,67]. den String „ABC“. Falls jedoch eine Zahl in der Liste dabei ist, welches nicht als Zeichen repräsentiert werden kann, so werden keine Zeichen ausgegeben. Um Elemente verschiedener Listen aneinanderzuketten kann der ++ Operator verwendet werden. Um Elemente zu entfernen --.

Bit Syntax – Erlang bietet mit der Bit Syntax die Möglichkeit Daten bitweise zu manipulieren. In Kombination mit Pattern Matching stellt die Bit Syntax eine beeindruckende Technik dar um mit Binärdaten zu arbeiten. Allgemein werden Binärdaten in der Form <<Segment1,...,SegmentN>> definiert. Ein Segment beschreibt dabei eine Sequenz von Bits. Standardmäßig hat ein Segment die Größe von 1 Byte (8 Bits). Segmente können unterschiedlich Groß sein. Die Bit Anzahl muss dabei nicht zwingend durch 8 teilbar sein. Ist die Bit Anzahl über alle Segmente durch 8 teilbar entspricht der Wert einem Binary, ansonsten wird von Bitstrings gesprochen. So kann der Wert 23 beispielsweise mit <<2#10111:5>>. auf 5 Bits gespeichert werden. Mit Pattern Matching kann ein Binary in entsprechende Segmente zerlegt und interpretiert werden. Der folgende Code zeigt dies beispielhaft anhand eines ICMP Pakets: <<Type:8,Code:8,Checksum:16,Data/binary>> = ICMP_Binary.. Die allgemeinere Form eines Segmentes ist Value:Size/TypeSpecifierList. Size und TypeSpecifierList sind dabei optional. TypeSpecifierList kann dann durch eine Reihe von Typ Spezifizierer definiert werden. Die formale Regel sieht dabei wie folgt aus: TypeSpecifierList = (integer|float|binary-)(signed|unsigned-)(big|little|native-)(unit:1–256)

Pattern Matching – Pattern Matching findet in Erlang folgende Anwendungen:

Allgemein ist die Schreibweise „Pattern = Expression“. Im Pattern können sowohl gebundene als auch ungebundene Bezeichner verwendet werden. Im Expression können gebundene Bezeichner, Werte, Funktionsaufrufe, oder mathematische Operationen vorhanden sein. Falls das Pattern auf den Ausdruck passt, werden ungebundene Bezeichner im Pattern mit den korrespondierenden Werten aus dem Ausdruck gebunden. Andernfalls bleiben die Bezeichner ungebunden, oder im Falle einer Funktion wird diese nicht aufgerufen. (Siehe Listing 2)

HttpRequest = { get,
[{content_type, "application/json"}],
"http://localhost:8080/"
}.
{Method, Headers, URL} = HttpRequest.
Listing 2: Pattern Matching

Module – Größere Projekte in Erlang können in Modulen strukturiert werden. Ein Modul enthält eine beliebige Sequenz von Attribut- und Funktionsdeklarationen. Modul Attribute können dazu verwendet werden um Eigenschaften eines Moduls im Format -attribut(Wert). zu definieren. Das Kompilat beinhaltet die definierten Modul Attribute, welche mit Module:module_info(attributes). abgefragt werden können. Es gibt jedoch Modul Attribute, welche vor den Funktionsdeklarationen platziert werden sollten, wie z.B. -module(Modulname)., -export(ExportierteFunktionen). und -import(Modul,Funktionen).. Mit dem Modul Attribut -on_load(Funktion) kann eine Funktion festgelegt werden, die automatisch aufgerufen werden soll, sobald das Modul geladen wurde. Implementiert ein Modul die Callback Funktionen für ein bestimmtes Behaviour, so muss dies durch ein Modul Attribut -behaviour(Behaviour). angegeben werden. Ähnlich wie in der Programmiersprache C bietet Erlang einen Präprozessor. Damit können beispielsweise mit -define(Konstante, Wert). Makros definiert werden, um den Code lesbarer zu gestalten. Der Zugriff auf den Inhalt des Makros Konstante erfolgt durch ?Konstante. Außerdem können andere Datei Inhalte innerhalb des Moduls durch den Befehl -include("MeineDatei.hrl"). mit einbezogen werden.

Funktionen – In Erlang sind Funktionen formal wie folgt strukturiert: <Head> [when Guards] -> <Body>.. Der Funktionsname wird als Atom, also ein Bezeichner der gleichzeitig seinem Wert entspricht, im Head festgelegt. Darauf folgen runde Klammern worin keine oder mehrere Argumente bzw. Patterns aufgelistet werden können. Das -> Zeichen trennt den Funktionskopf vom Inhalt. Es können mehrere Funktionsabschnitte durch ein Semikolon getrennt definiert werden. Jeder Funktionsabschnitt beinhaltet eine Deklaration und eine Definition. Jede Definition beinhaltet mindestens ein oder mehrere Ausdrücke, welche durch ein Komma separiert werden. Sobald eine Funktion mit den entsprechenden Parametern aufgerufen wird, wird Pattern Matching mit den Parametern und den Funktionsdeklarationen durchgeführt. Passen die Parameter mit den Patterns im Funktionskopf wird der entsprechende Funktionsabschnitt evaluiert. Reicht das Pattern Matching nicht aus um den Programmablauf zu definieren, können Guards verwendet werden. Nach einem erfolgreichen Pattern Matching werden die Parameter im Funktionskopf gebunden. Mithilfe von Guards können weitere Einschränkungen durch boolesche Ausdrücke bzgl. der Parameter erfolgen.

Selektionen (Case, if) – Es gibt in Erlang auch das case Konstrukt. Damit kann ein neuer Ausdruck gegen eine Reihe von Patterns (cases) überprüft werden. Die allgemeine Form ist in Listing 3 dargestellt.

case Ausdruck of
    Pattern1 [Guards1] -> Body1;
    
    PatternN [GuardsN] -> BodyN
end
Listing 3: Case, Guards

Der Ausdruck wird dabei sukzessive von Pattern1 bis PatternN überprüft. Sobald ein Pattern matched, wird der entsprechende Code evaluiert. Falls kein Pattern matched, entsteht ein Runtime Error. Die Kontrollstruktur if verhält sich sehr ähnlich zu case, nur gibt es dort keine Patterns sondern nur Guards, also boolsche Ausdrücke. Auf ähnliche Weise werden die Guards sukzessive ausgewertet. Sobald einer der Guards true ist wird der zugehörige Abschnitt ausgeführt. (Siehe Listing 4) Falls keine der Guards True ergibt, entsteht analog zum case ebenfalls ein Runtime Error.

if
    boolscherAusdruck1 -> Body1;
    
    boolscherAusdruckN -> BodyN
end
Listing 4: if, Guards

Recursion – Während case oder if Konkstrukte keinen Entwickler vom Hocker reißen, dürften einige sich über die fehlenden while oder for Schleifen wundern. In funktionalen Sprachen werden diese Schleifenkonstrukte in der Regel nicht angeboten. Stattdessen wird auf Rekursion aufgebaut, so auch in Erlang. Das Beispiel in Listing 5 zeigt den Einsatz von Pattern Matching und Rekursion anhand der Summen Funktion.

sum([]) -> 0;

sum([Kopf|Restliste]) -> Kopf + sum(Restliste).
Listing 5: Recursion
Falls die Funktion "sum" mit einer leeren Liste aufgerufen wird liefert sie die Zahl 0 zurück. Beinhaltet die Liste jedoch Elemente so werden die Parameter Kopf und Restliste durch Pattern Matching gebunden. "Kopf" beinhaltet dann den Wert des ersten Elements. Die Restliste repräsentiert alle weiteren Elemente der Liste ohne das erste Element. Dieser Funktionsabschnitt ruft sich rekursiv auf, die übergebene Liste jedoch wird immer kleiner, da jedes Mal das erste Element weggelassen wird. Dies wird solange wiederholt bis die Restliste keine Elemente mehr beinhaltet. Beim letzten Aufruf matched der erste Funktionsabschnitt, d.h. es gilt "[] = Restliste", und es wird 0 zurückgegeben. An dieser Stelle endet die Rekursion.

Tail Recursion – Eine rekursive Funktion führt in der Regel dazu, dass jeder Aufruf im Call Stack gespeichert wird. Falls solch eine Funktion nun endlos sich selbst aufrufen soll, kann der Call Stack schnell seine Grenzen erreichen. Dieses Problem wird in der funktionalen Welt durch Tail Recursion gelöst. Es muss lediglich beachtet werden, dass der letzte Ausdruck der Funktion einzig und allein der Selbstaufruf ist. Der Compiler ersetzt den letzten Selbstaufruf dann mit einem einfachen Sprung an den Funktionsanfang. Theoretisch kann die Funktion damit unendlich viele Selbstaufrufe machen. Das Code Beispiel in Listing 6 zeigt eine rekursive Funktion „loop“ und eine Tail rekursive Funktion „tail_loop“.

loop() ->
    loop(),
    1+1.

tail_loop() ->
    1+1,
    tail_loop().
Listing 6: Tail (and not) Recursion

Exceptions

Erlang unterscheidet Exceptions in den 3 Kategorien error, exit und throw. Jede Exception beinhaltet zudem noch ein Tupel, das den Grund der Exception und einen Stack Trace beinhaltet. Exceptions können nun durch try oder catch behandelt werden. Mit catch wird der Wert des Ausdrucks zurückgegeben. Im Falle einer Exception gibt catch ein Tupel mit den Informationen über die Exception zurück. Das try Konstrukt erweitert catch mit der Möglichkeit Pattern Matching auf die Exception Informationen anzuwenden. (Siehe Listing 7)

{Class, {Reason, Stacktrace}} = catch(1+a).

try 1+a
catch
    error:badarith -> "Ungültiger arithmetischer Ausdruck!"
after
    io:format ("Sollte auf jeden Fall ausgeführt werden.~n")
end
Listing 7: Exceptions

Elixir

Elixir ist eine junge Programmiersprache für die Erlang Virtual Machine (EVM). Die Syntax erinnert ein Wenig an Ruby. Die Programme können also in Elixir geschrieben und auf der EVM ausgeführt werden. Dazu wird der Elixir Source Code zu EVM kompatiblen Byte-Code kompiliert. Damit kann ein in Elixir geschriebenes Programm mit den in Erlang geschriebenen Bibliotheken interoperieren und vice versa. Mit Elixir soll die Entwicklung von verteilten Systemen mehr Spaß machen indem es effizientere Strukturen zur Organisierung des Codes anbietet und wiederkehrenden Code vermeidet. [2]

Fazit

Hochverfügbar, massiv parallele Verarbeitung, Aktoren, Open Source, Plattformunabhängigkeit, Big Data, automatische Speicherverwaltung es gibt kaum Buzzwords die man nicht in Erlang unterbringen kann und muss. Es ist spannend wie weit die Renaissance Erlangs gehen wird. Wird sich der kleine Bruder „Elixir“ durchsetzen? Falls wir unsere Begeisterung für Erlang vermitteln konnten und mit diesem Artikel Interesse geweckt haben sollten, dann müssen wir gleichzeitig warnen und entwarnen: Warnung deshalb, da es gilt sich auf drei Dinge einzulassen.

  1. Die Paradigmen der funktionalen Programmierung.
  2. Die Sprache Erlang an sich.
  3. Die OTP Bibliotheken und deren Funktionsweisen.

Entwarnung da Erlang nicht schwer ist. Unsere Empfehlung ist die hochwertige und amüsant zu lesende online Dokumentation [3], sowie ein umfangreiches außerordentlich gut geschriebenes Buch in deutscher Sprache. [4]

Quellen, Links und Interessantes

Referenzen

  1. Source Code, https://github.com/erlang/otp, Stand: 26.06.2014.  ↩

  2. Elixir, http://elixir-lang.org, Stand: 26.06.2014.  ↩

  3. Kostenfreies Erlang Buch, http://learnyousomeerlang.com, Stand: 26.06.2014.  ↩

  4. Erlang/OTP, Pavlo Baron, Open Source Press, 2012.  ↩