Programmieren mit Erlang – Teil 2: Programmierkonstrukte

Vor einer Woche haben wir in einem Schnelldurchlauf eine kleine Einführung in die funktionale Programmiersprache Erlang gegeben. Mit Sicherheit ist dem einen oder anderen dabei schon aufgefallen, dass Erlang durchaus einige Eigenheiten gegenüber anderen Programmiersprachen besitzt. So war es nicht möglich, den Wert einer Variablen zu ändern. Dagegen war es uns aber möglich, Verzweigungen durch die mehrfache Definition einer Funktion für Aufrufe mit speziell festgelegten Parametern. Heute soll es darum gehen, einige weitere Programmierkontrukte vorzustellen, die der Otto-Normal-Entwickler in Erlang verwenden kann.

Wie kann ich Schleifen in Erlang verwenden?
Spätestens an dieser Stelle wird mit Sicherheit der eine oder andere Programmierer, der bisher nur mit imperativen Programmiersprachen zu tun hatte, ein wenig enttäuscht sein über Erlang. Da dieses aber bekanntlich nun einmal eine funktionale Programmiersprache ist, welche mehr oder weniger auf dem mathematischen Funktionsmodell basiert, gibt es die üblichen Schleifen-Konstrukte for, while und do-while/do-until ganz einfach nicht. Zugegebenermaßen wäre aber auch genau dies wenig sinnvoll, wenn es ja schon nicht möglich ist, innerhalb einer Funktion den Wert einer Variablen zu verändern.
Somit ist es also notwendig eventuell itarativ entworfene Programmentwürfe zunächst einmal umzuarbeiten – Rekursion ist schließlich ein wichtiger Bestandteil in funktionalen Programmiersprachen und wie bereits beim letzten Mal im Beispiel gezeigt, auch in Erlang möglich.

Gibt es vielleicht noch andere Möglichkeiten für Verzweigungen?
Eine recht einfache Möglichkeit haben wir ja im 1. Teil bereits gezeigt. Ein weiteres einfaches Beispiel hierfür wäre auch die Berechnung der Fakultät:

[erlang]fak(0) -> 1;
fak(N) -> N * fak(N-1).[/erlang]

Das sieht ziemlich einfach aus und letztendlich ganz einfach nur die Definition der Fakultät abgetippt. Jedoch gibt es noch ein kleines Problem, welches es noch zu verhindern gilt. Dieses tritt nämlich auf, wenn der Nutzer nun ganz einfach einmal folgendes in die Erlang-Shell eingibt:
[erlang]module:fak(-1). % ersetze “module” durch den entsprechenden Modul-Namen[/erlang]
Rein mathematisch macht diese Eingabe natürlich keinen Sinn. Problematisch hieran ist nur, dass Erlang durch diese Eingabe in eine Endlosschleife gerät. Nachdem man also die Shell neugestartet hat (Dies kann man ganz einfach erreichen, indem man zunächst STRG + G drückt, dann mit j sich sämtliche Erlang-Jobs auflisten lässt, mit k *nummer* (*nummer* ist durch die entsprechende Job-Nummer der Shell zu ersetzen) die in Endlosschleife befindliche Shell tötet, dann mit s eine neue Shell startet und mit c *nummer* die neue Shell startet), kann man es nochmals auf neue Art und Weise versuchen. Diesmal verwenden wir ein weiteres Erlang-Konstrukt, welches ebenfalls die Eingaben auf bestimmte Werte prüft: sogenannte Guards (bzw. “Wächter”). Diese Guards werden wie folgt in die Funktion eingebunden:
[erlang]funktion(P1, P2, …, PN) when Guard1 -> Anweisungen1;
funktion(Q1, Q2, …, QN) when Guard2 -> Anweisungen2;
funktion(Q1, Q2, …, QN) when Guard3 -> Anweisungen3.[/erlang]
Solche Guards können dann wie folgt aussehen:
[erlang]is_number(X) % X ist eine Zahl
is_integer(X) % X ist eine ganze Zahl
is_float(X) % X ist eine Fließkommazahl
is_atom(X) % X ist ein Atom
is_tuple(X) % X ist ein Tupel
is_list(X) % X ist eine Liste
length(X) == 2 % Die Liste X hat 2 Elemente
size(X) == 2 % Das X ist ein 2-Tupel
X =< Y + Z % X ist kleiner oder gleich Y + Z X >= Y % X ist größer oder gleich Y
X == Y % X und Y haben gleichen Wert
X =:= Y % X und Y haben gleichen Wert und Typ
X /= Y % X ist ungleich Y[/erlang]
Zudem ist auch noch die logische Verknüpfung mehrerer solcher Ausdrücke mittels and, or, not, xor usw. möglich. Alternativ lassen werden mehrere Guards konjunktiv verknüpft (d.h. mittels and), wenn sie mit Kommas getrennt werden und disjunktiv verknüpft (d.h. mittels or), wenn sie mit Semikolons voneinander getrennt werden.
Zu beachten ist im Übrigen, dass es nicht möglich ist, selbstdefinierte Funktionen innerhalb von Guards zu verwenden – lediglich die von Erlang integrierten Funktionen oben (und noch einige weitere nicht ganz so wichtige) können in Guards verwendet werden.
Somit können wir das obige Beispiel minimal modifizieren, um die Endlosrekursion zu unterbinden:
[erlang]fak(0) -> 1;
fak(N) when N > 0 -> N * fak(N-1).[/erlang]
Beim selben Aufruf wie oben erscheint dann ganz einfach eine Fehlermeldung seitens Erlang, dass keine der Funktionen auf die Eingabe matcht.

Eine weitere Möglichkeit, die sicherlich dem einen oder anderen Programmierer wohl etwas mehr zusagt, weil diese sie schon aus den imperativen Programmiersprachen kennen, ist das if-Konstrukt. Die Syntax hierfür sieht in etwa so aus:
[erlang]if
Guard1 -> Anweisungen1;
Guard2 -> Anweisungen2;

GuardN -> AnweisungenN
end[/erlang]
Wie bei dem bekannten if-else-Konstrukt in C und Co. wird hier der erste Anweisungsblock ausgewertet, dessen Guard true liefert. Jedoch ist auch zu beachten, dass es notwendig ist, dass mindestens ein Guard zu true evaluiert, da sonst Erlang eine Fehlermeldung auswirft. Deshalb wird recht häufig als letzter Guard einfach “true” verwendet, welches semantisch dem else-Zweig entspricht.
Unsere Fakultätsfunktion von oben würden wir in etwa so gestalten:
[erlang]fak(N) -> if
N == 0 -> 1;
N > 0 -> N * fak(N-1);
true -> io:format(“Falsche Eingabe!”) % mit io:format lässt eine Ausgabe in der Shell erzeugen
end.[/erlang]

Da das if-Konstrukt durch die ausschließliche Verwendung von Guards als Bedingungen den Entwickler doch ein wenig einschränkt, gibt es noch ein weiteres Konstrukt, welches jedoch nicht semantisch äquvalent zu seinem Namensvetter aus den C-ähnlichen Programmiersprachen ist: Das case-Konstrukt. Während bei anderen Programmiersprachen nämlich alle Verzweigungsblöcke ab der ersten zutreffenden Bedingung auswertet, wird hier nur der eine Block ausgewertet, dessen Bedingung zutrifft (wie immer: Der erste zutreffende von oben). Die Syntax sieht nun in wie folgt aus:
[erlang]case Anweisung of
Pattern1 -> Anweisungen1;
Pattern2 when Guard -> Anweisungen2;

PatternN -> AnweisungenN
end[/erlang]
Ein Pattern ist letztlich nicht anderes als ein Funktionsparameter – also eine Variable oder ein spezieller Wert. Geprüft wird hier also ob die Auswertung der Start-Anweisung mit einem der folgenden Patterns übereinstimmt (wie beim if-Konstrukt muss wieder ein Pattern zutreffen, wenn keine Fehlermeldung von Erlang ausgeworfen werden soll. Ergänzend zu den Patterns können zudem auch noch Guards als weitere Bedingungen verwendet werden.
Wie das letztendlich in unserem Beispiel aussieht, seht ihr hier:
[erlang]fak(N) -> case N of
0 -> 1;
N when N > 0 -> N * fak(N-1);
_ -> io:format(“Falsche Eingabe!”)
end.[/erlang]

Was sind nun diese ominösen Höheren Funktionen?
Im 1. Teil dieser kleinen Erlang-Serie haben wir kurz die sogenannten Höheren Funktionen angesprochen. Das sind Funktionen, die entweder eine andere Funktion als Parameter erwartet oder eine andere Funktion als Rückgabewert ausgibt. Was zunächst eher nach Spielerei klingt, kann durchaus auch recht nützlich sein und wird in Erlang auch sehr viel verwendet (u.a. in diversen Listen-Operationen). Als Beispiel sei hier die map-Funktion genannt, die eine Funktion auf jedes einzelne Element einer Liste anwendet. Doch zunächst schauen wir uns erst einmal an, wie wir eine solche Funktion definieren:
[erlang]fun(Parameter1, …, ParameterN) -> Anweisungen end[/erlang]
Offensichtlich sehen die (anonymen!) Funktionen, die man als Parameter übergibt, nicht viel anders aus als jede beliebige andere Funktion. “fun” ist jedoch weder der Name dieser Funktion noch soll es sagen, dass die Funktion besonders viel Spaß machen muss, um verwendet werden zu können. Stattdessen ist dies eine einfache Bezeichnung für anonyme Funktionen. Weiterhin ist zu beachten, dass diese anonymen Funktionen immer mit einem “end” beendet werden.
Alternativ lassen sich auch bereits vorhandene Funktionen hierfür verwenden:
[erlang]fun modulname:funktionsname/X %ersetze X durch die Anzahl der Parameter der entsprechenden Funktion[/erlang]
Ausprobieren lässt sich das ganz einfach direkt über die Erlang-Shell. Hier kann man einfach einmal folgendes eingeben:
[erlang]F = fun(X) -> 3*X + 5 end.
F(4).[/erlang]
Die Variable F ist nun also offensichtlich eine einstellige Funktion. Da nun aber sicherlich der eine oder andere denkt “Wow! Wie toll, dass ich das kann”, wird es Zeit für eine sinnvolle Anwendung Höherer Funktionen: Programmieren wir doch einfach einmal die oben genannte map-Funktion! Dafür benötigen wir also eine Funktion mit zwei Parametern – einmal die Funktion und einmal die Liste. Da wir jedoch keine Schleifen verwenden können, müssen wir also versuchen, die Funktion rekursiv zu definieren. Dabei können wir im Funktionskopf zunächst einmal aus unserer Liste das jeweils erste Element auslesen, die Funktion darauf anwenden und das Ergebnis davon in eine neue Liste einfügen. Als Abbruchbedingung nehmen wir hierfür den Fall, dass in der Liste kein Element mehr enthalten ist – dann müssen wir nur noch eine leere Liste zurückgeben. Aber nun genug von der Idee, hier kommt erst einmal der Code:
[erlang]map(_, []) -> [];
map(F, [H|T]) -> [F(H)|map(F, T)].[/erlang]
In Zeile 1 befindet sich unsere Abbruchbedingung. Was hier auffällt ist, dass die Funktion F hier überhaupt gar nicht benötigt wird. Daher können wir sie auch einfach ignorieren und durch die anonyme Variable ersetzt werden.
In Zeile 2 splitten wir im Funktionskopf die Liste auf – in das erste Element H und die Restliste L. Der Rückgabewert von F angewendet auf H wird nun in eine neue Liste gespeichert, indem das erste Element eingefügt wird und dann durch rekursiven Aufruf die Restliste berechnet wird.
Dies wollen wir natürlich gleich einmal ausprobieren:
[erlang]modul:map(fun(X) -> 2*X end, [1,2,3,4,5]). %ersetze “modul” durch den Modulnamen[/erlang]
Nach etwas scharfen Hinsehen, dürfte man erkennen, dass die Funktion einfach die Zahlen von 1 bis 5 verdoppelt. Und tatsächlich gibt uns Erlang hier auch die Liste [2,4,6,8,10] aus.
Damit im Übrigen nicht jeder diese Funktion neu programmieren muss, wenn er sie einmal benötigt, ist die map-Funktion bereits standardmäßig in Erlang enthalten – und zwar als lists:map/2

Geht das vielleicht auch einfacher?
In Erlang gibt es tatsächlich noch ein weiteres Konstrukt, welches es ermöglicht, eine Operation auf jedes Element – oder auch nur eine Teilmenge – einer Liste anzuwenden: Sogenannte List-Comprehensions. Ob diese nun einfacher sind oder nicht, das muss jeder für sich selbst entscheiden (ich persönlich mag sie nicht wirklich). Von der Syntax-Seite sehen die List-Comprehensions so aus:
[erlang][Anweisung || Qualifier1, Qualifier2, …, QualifierN][/erlang]
Ein Qualifier kann sein:
(1) ein Generator: Generatoren wählen Elemente aus einer Liste aus. Die Syntax hierfür ist:
[erlang]Pattern <- Liste[/erlang] (2) ein Filter: Filter sind Funktionen oder Ausdrücke, die einen bool'schen Rückgabewert haben. Etwas einfacher gesagt, wird die Anweisung im vorderen Teil auf jedes Element aus einer Liste, die die Bedingungen im hinteren Teil der List-comprehension erfüllen, angewendet. Hierbei muss jedoch beachtet werden, dass die Liste nicht unednlich sein darf, da Erlang sonst bis in alle Ewigkeiten rechnen würde. ;-) Unsere map-Funktion würde mit den List-Comprehensions nun also so aussehen: [erlang]map(F, L) -> [F(X) || X <- L].[/erlang] Der Quelltext dieser Funktion ist somit noch einmal kürzer geworden, dafür aber auch schlechter lesbar. Deshalb hier noch einmal eine kurze Erklärung: Im hinteren Teil der List-Comprehension wird ein Element X aus der Liste L ausgewählt (da kein weiterer Qualifier vorhanden ist, werden alle Elemente aus L nacheinander ausgewählt), auf dieses wird im vorderen Teil die Funktion F angewendet. Damit sind wir also in der Erlang-Programmierung wieder ein ganzes Stück weiter gekommen und können nun schon praktisch alles, was die übliche imperative Programmiersprache kann, auch schon zusammenbasteln - wenn es auch manchmal etwas mehr Aufwand benötigt, weil man zunächst überlegen muss, wie man vom iterativen zum rekursiven Algorithmus kommt. Dies ist jedoch trotzdem noch nicht das Ende dieser Serie rund um die Programmierung in Erlang. Im nächsten Teil werden wir nämlich noch einmal auf die Paradigmen der nebenläufigen und verteilten Programmierung eingehen - der Grund, warum Erlang doch für so manchen Programmierer attraktiv ist. Quelle: Teile dieses Artikels wurden inspiriert von Prof. Schäfers Vorlesung “Programmierparadigmen” an der TU Ilmenau sowie der Dokumentation auf erlang.org.

Ein Kommentar

Pingbacks: 1

pingback von Programmieren mit Erlang – Teil 3: Besonderheiten « Netroid 31. Juli 2012

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*