Donnerstag, 24. Mai 2012


Artikel

Oktober 2010 | Artikel

Advanced Scala: Tail Recursion und Optimierung

(Link zum Artikel: http://www.entwickler.de/jaxenter//003405)

Advanced Scala – Teil 5

Text: Heiko Seeberger
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
In der imperativen Programmierung, die im "Standard-Java" fast ausschließlich zur Anwendung kommt, verwenden wir oft Schleifen und ändern darin den Zustand eines Objekts bzw. einer Variable. Viele Probleme lassen sich zumindest konzeptionell einfacher mit dem Prinzip der Rekursion [1] lösen. Wir wollen als Beispiel die Summenbildung über eine Liste von Int-Zahlen betrachten.
Teil 1   Teil 2   

In Java könnte eine typische Implementierung folgendermaßen aussehen:

  1. public static int sum(List<Integer> numbers) {
  2. int result = 0;
  3. for (int number: numbers) {
  4. result += number;
  5. }
  6. return result;
  7. }

Das können wir natürlich auch in Scala analog umsetzen:

  1. def sum(numbers: List[Int]): Int = {
  2. var result = 0
  3. for (number <- numbers) {
  4. result += number
  5. }
  6. result
  7. }

Wie sieht nun eine rekursive Alternative aus?

  1. def sum(numbers: List[Int]): Int = numbers match {
  2. case Nil => 0
  3. case x :: xs => x + sum(xs)
  4. }

Zunächst fällt auf, dass diese Implementierung kürzer ist. Und wenn man sich erst einmal an die rekursive Denkweise gewöhnt hat, dann erkennt man schnell, dass dieser Weg auch einfacher zu verstehen ist. Denn "Die Summe über eine Liste ist das erste Element plus die Summe über den Rest" entspricht viel eher dem intuitiven Verständnis als "Nimm eine Speicherzelle, iteriere über alle Elemente und addiere diese zum aktuellen Wert der Speicherzelle".

Noch deutlicher wird der Vorteil der Rekursion bei den Fibonacci-Zahlen [2], denn diese sind ja rekursiv definiert:

  1. fib(n) =
  2. 0 für n = 0
  3. 1 für n = 1
  4. fib(n-2) + fib(n-1) für n > 1

Das geht noch besser!
So intuitiv und elegant Rekursionen auch sein mögen, unter der Haube sind sie nicht unproblematisch. Denn grundsätzlich wird bei jedem Aufruf ein neuer Stack Frame erzeugt, was mit Zeit und Stack-Speicherplatz verbunden ist. Gerade bei Scala ist das ein sehr ernst zu nehmendes Problem, denn der Stack ist auf der JVM typischerweise eher klein ausgelegt. Und selbst wenn wir quasi beliebig viel Stack zur Verfügung stellen, ist die Performance doch fast immer ein wichtiges Thema.

Daher ist es entscheidend, dass uns der Compiler und die Ablaufumgebung so gut wie möglich mit Optimierungen [3] unterstützt. Besonders großes Optimierungspotenzial bieten Tail Recursions bzw. Endrekursionen [4], d. h. solche rekursiven Funktionen, deren letzte Aktion zur Ermittlung des Rückgabewerts der rekursive Aufruf ist. In diesem Fall kann der Compiler die Rekursion durch eine Iteration ersetzen, sodass der Stack kein Problem mehr darstellt; eine solche Optimierung wird Tail Call Optimization genannt.

Die Java Virtual Machine lässt für Scala leider nicht alle Spielarten von Tail Call Optimizations zu. Beispielsweise ist es nicht möglich, sich wechselseitig endrekursiv aufrufende Funktionen zu optimieren. Aber natürlich werden alle Funktionen optimiert, die sich selbst endrekursiv aufrufen.

Teil 1   Teil 2   

andere Artikel dieser Serie

Kommentare

Gravatar Kyle 19.10.2010
um 15:35 Uhr
Was noch interessant wäre: wie sieht der Vergleich zur iterativen Java und Scala Variante aus? Ist es eine nette Spielart für Leute, die aus der funktionalen Programmierung kommen oder ist ein Vorteil auch für imperative Programmierer gegeben? #zitieren
Gravatar Ralf 19.10.2010
um 19:48 Uhr
Ist jetzt etwas enttäuschend. Zuerst werden einem lange Zähne gemacht mit einer eleganten kurzen Rekursion und dann wird der Pferdefuß hervorgekramt, damit das überhaupt laufen kann. Und dann sieht das gar nicht mehr einfach aus und leicht verständlich auch nicht. Schade. #zitieren
Gravatar Steve 20.10.2010
um 14:45 Uhr
Tja, wird Zeit, dass die Oracle-Leute endlich in die Gänge kommen.

Wäre tailcall-Unterstützung in der VM, würde es sich mal lohnen HotSpot die Transformation von nicht-end-rekusive Funktion zu end-rekursiven Methoden beizubringen. Dann könnte man die Sachen ganz normal hinschreiben.

Aber so wie es aussieht ist tailcall-Support auf jeder Prioritätenliste immer an zweiter Stelle und was "wichtigeres" findet man anscheinend immer ...
#zitieren
Gravatar Joe 20.10.2010
um 14:54 Uhr
Wer programmiert denn so in einer Sprache mit higher-order Konstrukten? #zitieren
Gravatar stone 20.10.2010
um 16:17 Uhr
Ich schließe mich Ralf an, aus 8 Zeilen für jeden verständlichen Code würden 10 die erst mal viele Fragen aufwerfen. Auserdem in einer Funktionalen sprache angst vor recursionen haben schon witzig. #zitieren
Gravatar Sam 21.10.2010
um 05:20 Uhr
Das ist eine Standard-Technik, man führt einen Akkumulator ein. Da gibt es weit seltsamere und unverständlichere Idiome in Java. #zitieren
Gravatar gruenewa 21.10.2010
um 22:46 Uhr
Um die Bedeutung der Tail-Call Optimization noch stärker zu verdeutlichen, hätte man eventuell noch zeigen können, wie sich die im Artikel gezeigte Standard-Technik aus Pattern-Match und Rekursion mit Hilfe von High-Order Functions als eine Fold Operation verallgemeinern lässt. Die Summe berechnet sich dann:

numbers reduceLeft(_+_)
#zitieren