Programmiersprachen-Enzyklopädie

Erlang: Renaissance des Paralleluniversums
Kommentare

Warum das im Stillen gereifte Erlang eine Renaissance erlebt und sich nicht nur als Vorbild für neue Sprachen größerer Aufmerksamkeit erfreuen wird, erklärt dieser Artikel.

Konnte Joe Armstrong, der geistige Vater von Erlang, vor 25 Jahren schon ahnen, welchen bemerkenswerten Weg seine Kreation nehmen wird?

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 stand jedoch aufgrund mangelnder Popularität funktionaler Programmiersprachen, nebst sämtlichen Hypes rund um die Objektorientierung, meist abseits des Rampenlichts. 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 Softwareentwicklung, 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 Multi-Threading bzw. in der horizontalen Skalierung.

Mittlerweile gibt es in Java viele Frameworks und Sprachneuerungen, die uns die Arbeit in diesen Bereichen erleichtern sollen – mit Erfolg. 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 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 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 rund um Erlang 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“, heißt es da. Dennoch dominiert Respekt und Anerkennung bezüglich Stabilität und Robustheit in den schweren Disziplinen der massiven parallelen Verarbeitung. Harte Fakten wie erreichte 99,9999999 Prozent Verfügbarkeit über zwanzig 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 einen oder anderen Leser womö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. 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 unter anderem auch als Middleware fungieren, verwendet. Der Überbegriff Open Telecom Platform (OTP) sollte an dieser Stelle andere Branchen allerdings nicht abschrecken. Der Name stammt aus dem ursprünglichen Use Case, nämlich einem der ersten Anwendungsfälle, bei dem massive und stabile Echtzeitparallelverarbeitung 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 so genannte Behaviours, welche die Erlang-Prozesse klassifizieren und Callbacks für die individuelle Ausgestaltung bereitstellen. Besonders populär ist der Supervisor, welcher abstürzende Prozesse wieder zum Leben erweckt (Abb. 1).

Das „Let it crash“-Prinzip im schematischen Überblick

Abb. 1: Das „Let it crash“-Prinzip im schematischen Überblick

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 zuhause. Dabei liegt der Fokus klar auf technischen Anforderungen: Cloud-Server, Webserver, Chatsysteme, Onlinespiele-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 grafischen 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 Testunterstützung.

Erlangs Brillanten

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 so genannter 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 Supervisorprozess 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önnten, gedacht – sondern es wird akzeptiert, dass dies gar nicht möglich ist. Aber wenn etwas Schlimmes passiert, dann weiß Erlang damit umzugehen.
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 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 dazugekauft 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 Prozent.
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]).

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 Tupeln 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, welche nicht als Zeichen repräsentiert werden kann, so werden keine Zeichen ausgegeben. Um Elemente verschiedener Listen aneinander zu ketten, kann der ++-Operator verwendet werden. Um Elemente zu entfernen –.

Bitsyntax

Erlang bietet mit der Bitsyntax die Möglichkeit, Daten bitweise zu manipulieren. In Kombination mit Pattern Matching stellt die Bitsyntax 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 einem Byte (8 Bits). Segmente können unterschiedlich groß sein. Die Bitanzahl muss dabei nicht zwingend durch acht teilbar sein. Ist die Bitanzahl über alle Segmente durch acht teilbar, entspricht der Wert einem Binary, ansonsten wird von Bitstrings gesprochen. So kann der Wert 23 beispielsweise mit <<2#10111:5>>. auf fünf 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 Segments ist Value:Size/TypeSpecifierList. Size und TypeSpecifierList sind dabei optional. TypeSpecifierList kann dann durch eine Reihe von Typspezifizierern definiert werden. Die formale Regel sieht dabei wie folgt aus: TypeSpecifierList = (integer|float|binary-)(signed|unsigned-)(big|little|native-)(unit:1-256).

List und Bitstring Comprehensions

List bzw. Binary Comprehensions lehnen sich typischerweise stark an die Schreibweise für Mengen aus der Mathematik an (set comprehensions). Dabei wird eine Menge nicht durch eine Auflistung der Elemente, sondern durch das Angeben von Eigenschaften bezüglich der enthaltenen Elemente definiert. Die mathematische Comprehension {x:Z | 0<x<5 : 2*x} beschreibt beispielsweise die Menge {2,4,6,8}. In Erlang würde diese Menge mit [2*X || X<-[1,2,3,4]]. definiert werden. Einzelne Elemente können durch zusätzliche Bedingungen herausgefiltert werden. Die Bedingungen können am Ende der Comprehension kommagetrennt angefügt werden. Mit [2*X || X<-[1,2,3,4], X rem 2 =:= 0]. werden beispielsweise nur die geraden Zahlen aus der Menge berücksichtigt und verdoppelt, was [4,8] ergibt. Eine allgemeinere Form von List Comprehensions ist [Ausdruck || Generator1,…,GeneratorN,Bedingung1,…BedingungN]. Binary Comprehensions funktionieren analog zu List Comprehensions und unterscheiden sich nur minimal in der Notation.

Pattern Matching

Pattern Matching findet in Erlang folgende Anwendungen:

• Binden von Werten zu Bezeichnern
• Steuerung des Programmablaufs
• Extraktion von Werten aus Tupel/Listen

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 (Listing 2).

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

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 Dateiinhalte 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 beziehungsweise 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 einen 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 zu 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 bezüglich 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

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 boolesche Ausdrücke. Auf ähnliche Weise werden die Guards sukzessive ausgewertet. Sobald einer der Guards true ist, wird der zugehörige Abschnitt ausgeführt (Listing 4). Falls keine der Guards true ergibt, entsteht analog zum case ebenfalls ein Runtime-Error.

if
    boolscherAusdruck1 -> Body1;
    ...
    boolscherAusdruckN -> BodyN
end

Recursion

Während case- oder if-Konstrukte 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 Summenfunktion.

sum([]) -> 0;
sum([Kopf|Restliste]) -> Kopf + sum(Restliste).

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 Codebeispiel 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().

Exceptions

Erlang unterscheidet Exceptions in den drei 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 (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

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-Sourcecode zu EVM kompatiblem Bytecode 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.

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, weil man sich auf drei Dinge einlassen muss:

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 Onlinedokumentation, sowie ein umfangreiches außerordentlich gut geschriebenes Buch in deutscher Sprache.

Aufmacherbild: Michelangelo God’s touch. Close up of human hands touching with fingers von Shutterstock / Urheberrecht: Sergey Nivens

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -