Wer mit funktionaler Programmierung beginnt und sich Code-Beispiele
anschaut, bemerkt schnell, dass die Code-Beispiele
selten for- oder while-Schleifen verwenden. Dieser Artikel
beschreibt in Scala, was funktionale Programmierer statt Schleifen
verwenden und zeigt dabei, was es mit Endrekursion auf sich hat.
Java-Programmierer sind es gewohnt, for-Schleifen zu verwenden, wenn
sie über den Inhalt einer Collection iterieren möchten. Hier ein
Beispiel
aus dem Projekt Storm:
readCommandLineOpts wandelt die System-Eigenschaft „storm.options“
der Art „foo=13,bar=22“ in eine Map „{foo -> 13, bar -> 22}“ um.
Kernstück ist dabei eine for-Schleife, welche aus den Konfigurationen
in der System-Eigenschaft die Einträge in der Map erzeugt.
Natürlich kann man so ähnlich auch in Scala programmieren:
Hier wird die for-Schleife verwendet, um Einträge in der
Ergebnis-Map ret via Seiteneffekt zu erzeugen. Eine Schleife, die
wegen ihrer Seiteneffekte aufgerufen wird, stellt man in Scala am
besten mit foreach dar:
foreach erhält dabei eine Funktion als Argument, die sie für jedes
Element einer Iterable Collection aufruft. Ich finde,
readCommandLineOpts2 lässt leichter erkennen, was mit
commandOptions passiert: zunächst werden Zeichen ersetzt und an
Kommata getrennt, und anschließend wird jedes Element einzeln
behandelt. Man kann den Code „von links nach rechts“ lesen. Die
Verkettung von replaceAll und split ist in Java natürlich auch
möglich.
Ähnlich zu foreach gibt es in Scala und anderen funktionalen
Programmiersprachen eine Reihe von Funktionen, welche die üblichen
Anwendungsfälle von Schleifen ohne Seiteneffekte abdecken:
`filter(p)` filtert Elemente aus einer Collection, die eine gegebene
Bedingung *p* erfüllen.
`count(p)` zählt die Elemente einer Collection, welche eine gegebene
Bedingung *p* erfüllen.
`take(n)` liefert die ersten *n* Elemente einer Collection.
`drop(n)` liefert alle Elemente einer Collection *bis auf die
ersten n*.
`takeWhile(p)` liefert die ersten Elemente einer Collection, die
alle der Bedingung p genügen.
`dropWhile(p)` liefert alle Element einer Collection, bis auf die
ersten, welche der Bedingung p genügen.
`forall(p)` prüft, ob eine Bedingung auf alle Element einer
Collection zutrifft.
`contains(p)` prüft, ob eine Bedingung auf irgend ein Element einer
Collection zutrifft.
`splitAt(n)` teilt eine Collection an der Position *n* in zwei
Teile.
`partition(p)` teilt eine Collection in die zwei Teile, welche der
Bedingung p genügen oder nicht genügen.
`foldLeft(start)(op)` ist quasi das Schweizer Taschenmesser und
steht für folgenden Code-Block:
Damit lassen sich praktisch alle for-Schleifen ersetzen.
In der [Einführung in die rein funktionale Programmierung](http://funktionale-programmierung.de/2013/04/10/rein-funktional-2.html)
hatten wir `foldLeft` als `foldl` der Programmiersprache Racket vorgestellt.
`reduceLeft(op)` entspricht `foldl(x0)(op)`, wobei x0 das erste
Element der Collection ist.
`scanLeft(start)(op)` ist ähnlich zu `foldl`, nur dass es auch alle
Zwischenergebnisse liefert statt nur dem Endergebnis.
`map(f)` wendet die Funktion *f* auf jedes Element einer Collection
an und liefert die Ergebnisse in einer neuen Collection, wobei eine
etwaige Reihenfolge erhalten bleibt.
`collect(pf)` wendet die partielle Funktion *pf* nur auf die
Elemente einer Collection an, für die *pf* definiert ist und liefert
die Ergebnisse in einer neuen Collection, wobei eine etwaige
Reihenfolge erhalten bleibt.
Meiner Erfahrung nach lässt sich mit diesem „Arsenal“ an Funktionen
Code lesbarer gestalten: statt lauter for-Schleifen stehen Verben, die
ausdrücken, was die Schleifen erreichen wollen.
Mit map und collect lässt sich das eingangs erwähnte
readCommandLineOpts wie folgt schreiben:
Nach dem split an den Kommas, trennt map jedes Element nochmals am
Gleichheitszeichen. collect sammelt dann alle Elemente ein, die aus
zwei Werten bestehen und wandelt sie in ein Schlüssel-Werte Paar um.
Die Collection aus Schlüssel-Werte Paaren wird dann zum Schluss
mittoMap in eine Map verwandelt.
Ein häufiger Einwand an dieser Stelle ist, dass „unnötige“
Zwischenergebnisse mit einer sehr kurzen Lebensdauer erzeugt werden.
Das stimmt, in der Praxis ist dies jedoch genauso häufig nicht
relevant. Zum Beispiel kann man bei readCommandLineOpts davon
ausgehen, dass es weder viele Kommando-Zeilen Optionen gibt, noch dass
sie besonders groß sind. Außerdem können Compiler oder Bibliotheken
die Zwischenergebnisse bei rein-funktionalem Code wegoptimieren.
Sollte Effizienz dennoch relevant sein, kann man natürlich immer noch
die Lesbarkeit der Effizienz opfern und Schleifen verwenden.
Allerdings bevorzugt man in der funktionalen Programmierung rekursive
Funktionen oder genauer gesagt endrekursive Funktionen.
Endrekursive Funktionen sind Funktionen, bei denen der rekursive
Aufrufe „an letzter Stelle“ kommt, das heißt, das Ergebnis des
rekursiven Funktionsaufrufs wird nicht weiter in einer Berechnung
verwendet.
Für das Beispiel des readCommandLineOpts könnte eine endrekursive
Hilfsfunktion loop wie folgt aussehen:
readCommandLineOpts5 erzeugt keine unnötigen Zwischenergebnisse
mehr. Der erzeugte Bytecode zeigt, dass der rekursive Aufruf von
loop in der Zeile 86 in ein simples GOTO übersetzt wird:
Damit können Programmierer wählen, wie lesbar oder effizient sie ihre
Schleifen gestalten wollen.
Wer in die funktionale Programmierung einsteigt, dem kann ich
empfehlen, womit ich ebenfalls begonnen hatte: bei jeder for- oder
while-Schleife sich fragen, ob nicht eine der obigen Funktionen
deutlicher ausdrückt, was der Code erreichen möchte. Als Ergebnis
wartet ein leichter lesbarer und wartbarer Code.
Andreas Bernauer
Dr. Andreas Bernauer ist Experte für funktionale Softwarearchitektur. Er beschäftigt sich seit über 10 Jahren mit funktionaler Programmierung.