Weg mit den Schleifen!

Für zahlreiche Entwickler stellen Schleifen ein altmodisches Werkzeug dar, das abgeschafft gehört. Dafür sprechen viele gute Argumente, aber auch ein paar dagegen.

In Pocket speichern vorlesen Druckansicht 341 Kommentare lesen
Weg mit den Schleifen!
Lesezeit: 26 Min.
Von
  • Marco Emrich
Inhaltsverzeichnis

Gute Softwareentwickler erweitern ständig ihren metaphorischen Werkzeugkasten, um neue oder bessere Werkzeuge für spezifische Situationen kennen zu lernen. Dazu gehören beispielsweise Entwicklungsumgebungen, CLI-Tools, Bibliotheken, Frameworks, ganze Programmiersprachen oder auch einzelne Sprachfeatures. Sicherlich wäre es hilfreich, sich hin und wieder von mentalem Ballast zu befreien und den ein oder anderen veralteten "Steinkeil" wieder los zu werden. Damit bleibt der Werkzeugkasten langfristig leicht und flexibel.

Für einige Entwickler stellen klassische Schleifen wie for oder while einen solchen Steinkeil dar, den sie abschaffen wollen.

Ende der 60er Jahre sorgte ein Artikel für Aufruhr, der den Titel "Goto considered harmful" [1] trug. Darin formulierte Dijkstra seinen Angriff auf das klassische Sprachfeature Goto. Dass inzwischen Goto nahezu verschwunden ist, unterstreicht den Erfolg des Artikels oder zumindest die Gültigkeit der Kernaussage. Damals sahen die Reaktionen allerdings anders aus. Neben Erstaunen und Beifall erntete der Artikel vor allem eines: Erzürnte Kommentare aufgebrachter Leser – aus heutiger Sicht ein Flamewar. 1987 erschien der Gegenartikel "'Goto considered harmful' considered harmful" [2] und wurde noch im gleichem Jahr von "'"GOTO Considered Harmful" Considered Harmful' Considered Harmful?" [3] gekontert – kein Scherz.

Insgesamt muss die Situation derart verschärft gewesen sein, dass die ACM (Association for Computing Machinery) Stellung bezog: Sie würden etwas Derartiges nie wieder veröffentlichen.

Ein emotionaler Aufruf, der mit Fackeln und Mistgabeln bewaffnete Schleifenbefürworter auf den Plan ruft, ist wenig hilfreich. Deswegen versucht es der vorliegende Artikel mit einer ausgewogenen Betrachtung, die sich durchaus der Schwächen von Schleifenalternativen bewusst ist und Situationen identifiziert, in denen Schleifen ihren Wert haben.

Welchen Grund gibt es nun, klassische Schleifen zu vermeiden? Warum ist es sinnvoll, Alternativen zu einem Sprachfeature zu suchen, das Entwickler seit über 50 Jahren erfolgreich einsetzen? Schleifen bringen viel Ballast mit, der nur kaum auffällt. Für erfahrene Entwickler ist der Einsatz von Schleifen zur zweiten Natur geworden – und die Probleme dadurch unsichtbar.

Im Grunde lassen sich folgende vier Hauptprobleme identifizieren:

  • +/-1-Probleme
  • Endlosschleifen
  • Statusbehaftung
  • Versteckte Absicht

Äußerst unbeliebt und weit verbreitet ist das sogenannte +/-1-Problem (engl. Off-by-one-Error), für das vor allem for-Schleifen sehr anfällig sind:

const a = [1, 2, 3, 4, 5];

for (let j = 1; j <= a.length; ++j) {
console.log(a[j]);
}

Diese in JavaScript genutzte for-Schleife sollte die Elemente aus dem Array ausgeben. Es gibt aber gleich zwei Fehler: Der Index startet bei 1 statt bei 0, und die Bedingung müsste < statt <= verwenden. Letzteres ist trotz des einfachen Beispiels etwas schwieriger zu entdecken. Schleifen in realem Code sind oft viel komplexer und erschweren die Fehlersuche.

Eng damit verbunden ist das absurde sogenannte Voodoo Chicken Coding – ein klassisches Antipattern. Dabei versuchen Entwickler einen Bug nicht durch Verstehen des Codes zu finden, sondern die Reparatur besteht darin, hier und da kleine Änderungen am Code vorzunehmen: Ein +1 hier, ein -1 da. Diese "Beschwörungen" können den Code durchaus wieder zum Laufen bringen, sind aber alles andere als förderlich für die Codequalität.

Endlosschleifen sind die typische Ursache für Programme, die hängen bleiben und nicht mehr auf Eingaben reagieren. Sie können oft sehr subtile Ursachen haben und sind schwer zu entdecken.

let j = -10;
while (j < 80);
j *= -2;

Ist das obige JavaScript-Beispiel eine Endlosschleife oder nicht? Das Beispiel stammt aus einem Quiz. Das eigentliche Problem besteht darin, dass die while-Anweisung am Zeilenende mit einem Semikolon ";" abschließt. In JavaScript ist das eine leere Anweisung. Die while-Anweisung führt also nie die Zeile aus, die die Variable j verändern und somit die Schleife letztlich terminieren könnte. Sattdessen führt while die leere Anweisung bis in alle Ewigkeit oder zum Abbruch des Programms aus.

Schleifen zu debuggen ist ein ganz besonderer "Spaß". Vielleicht tritt das Problem erst bei Durchlauf 4117 auf und nur, wenn die Variablen eine bestimmte Belegung haben – und es draußen regnet. Letzteres ist zwar übertrieben, aber erfahrene Entwickler wissen, dass das Debuggen eine zeitaufwendige und nervenraubende Tätigkeit ist. Wieso ist ein Debugger überhaupt nötig? Was ist das zugrundeliegende Problem?

Das Kernproblem ist der "State". Statusbehaftete Anweisungen führen dazu, dass sich der geschriebene Code (static) vom Lauftzeitverhalten (runtime) unterscheidet. Dadurch müssen Entwickler Statusänderungen wie Variableninhalte im Debugger beobachten.

Es ist freilich schwierig und nicht immer sinnvoll, State komplett zu vermeiden. Das können nur rein funktionale Programmiersprachen wie Haskell leisten. Dennoch hilft es der Lesbarkeit, State zu reduzieren. Klassische Schleifen sind unglücklicherweise der Inbegriff statusbehafteter Programmierung. Böse Zungen könnten sagen, Schleifen sind geradezu statusverseucht.

Das folgende Beispiel hilft, das Problem der schwer erkennbaren oder versteckten Absicht (hidden intent) zu verdeutlichen. Der Code soll alle geraden Zahlen aus einem Array herausfiltern (Abb. 1). Eine Funktion, die feststellen kann, ob eine Zahl gerade ist, ist vorgegeben:

const even = n => n % 2 === 0

even gibt true zurück, falls es sich um eine gerade Zahl handelt, sonst false. Um mit einer Schleife zu filtern, ist folgender Code nötig:

let a = [1, 2, 3, 4, 5, 6];
let r = [];

for (let j = 0; j < a.length; ++j) {
if (even(a[j]) {
r.push(a[j]);
}
}

Dabei ist a das Ausgangsarray, während die Variable r[/r] dem Auffangen der Ergebnisse dient. Schließlich kommt die Schleife zum Einsatz, die anhand des Index [i]i alle Elemente durchläuft und nach der if-Abfrage nur gerade Zahlen in das Ergebnisarray schiebt.

Aufgabe: Filtere aller geraden Zahlen aus dem Array

Wie sieht eine Alternative ohne Schleife aus? Sinnvoll ist hier die Higher-Order-Funktion filter:

let r = [1, 2, 3, 4, 5, 6].filter(even)

Das Ergebnis ist dasselbe. Bei der Umsetzung mit der Schleife ist allerdings die komplette Mechanik des Vorgangs sichtbar, während die Higher-Order-Funktion lediglich die eigentliche Aufgabe verdeutlicht. Es ist einige Zeit nötig, um die Absicht hinter der Schleife zu verstehen. Die Alternative mit .filter(even) kommuniziert die Absicht direkt: Filtern der geraden Zahlen! Es ist keine gedankliche Transformation vom geschriebenen Code zum Erkennen der Aufgabe erforderlich. Das spart wertvolle Zeit beim Lesen.

Freilich handelt es sich um ein einfaches Beispiel, und erfahrene Entwickler sind darauf trainiert, solche Muster (konkret ein if in einer Schleife) schnell zu erkennen. In der Praxis ist Code aber oft deutlich komplexer, das Verstehen aufwendiger. Der Zeitaufwand zum Erfassen von Schleifen summiert sich bei realem Code schnell auf.

Funktionale Programmierer sprechen bei der Filterlösung oft von deklarativem Code oder "Was gegenüber Wie?". Bei der Schleife ist es nötig, die Mechanik (wie) genau zu verstehen, während die Alternative nur auf die eigentliche Absicht abstrahiert (was). Anders ausgedrückt: Die Schleife versteckt die Absicht (Intent) in einem Wust aus Mechanik.