Teil 7: for-Expressions

Wir haben mit Schleifen und Iteratoren bereits eine Möglichkeit kennen gelernt um über Collections zu iterieren. Die for-Expression geht aber weit über die bereits kennen gelernten Möglichkeiten hinaus weshalb sie in keinem Repertoire eines Scala-Entwicklers fehlen sollte.

Die for-Expressions wird manchmal auch als for-Comprehension bezeichnet – sie wird aber nie als for-Schleife bezeichnet, denn sie ist keine Schleife. Sie mag vielleicht wie eine Schleife funktionieren, intern iteriert sie aber über Collections und wendet verschiedene Operationen darauf an. Weiterhin gibt sie – wie alles in Scala – ein Ergebnis zurück mit dem man später weiterarbeiten kann.

In Verbindung mit der for-Expression begegnen wir tatsächlich einem Operator, der keine Methode ist. Stattdessen ist er ein Schlüsselwort der Sprache:

scala> val xs = Seq(1, 2, 3)
xs: Seq[Int] = List(1, 2, 3)

scala> for (x <- xs) println(x)
1
2
3

Das `<-` bindet die Variable `x` an die Collection `xs`. Dass `<-` keine Methode ist erkennt man wenn man versucht es in `object notation` zu schreiben:

scala> for (x.<-(xs)) println(x)
<console>:1: error: identifier expected but '<-' found.
       for (x.<-(xs)) println(x)
              ^

Man kann die Zuweisung in etwa so lesen: Iteriere über die Collection `xs` und binde jedes Element nacheinander an die Variable `x`, damit sie zur weiteren Verarbeitung zur Verfügung steht.

Die deklarierte Variable ist nur innerhalb der for-Expression gültig, von außerhalb kann nicht auf sie zugegriffen werden. Es ist außerdem nicht nötig dem Compiler mit `val` or `var` mitzuteilen, dass wir eine Variable erzeugen wollen – das erkennt er selbst. Um noch ein wenig tiefer ins Detail zu gehen: Eigentlich haben wir hier gar keine Variablendeklaration. Man spricht mehr von Generator. Der Grund warum dies so genannt wird liegt in der Implementierung der for-Expression, aber dazu später mehr.

Wenn unsere for-Expression nur eine Anweisung im Körper besitzt, können die geschweiften Klammern ausgelassen werden – aber das sollte soweit ja schon bekannt sein. Diese Standardversion der for-Expression arbeitet ein wenig wie die foreach-Schleife aus Java und sieht auch so ähnlich aus. Scalas for-Expression kann aber noch weitaus mehr als Javas Pendant.

Wir haben z.B. eine Liste mit Ints und wollen alle Elemente verdoppeln. In Java könnte das so aussehen:

List<Integer> doubleValues(List<Integer> list) {
  List<Integer> ret = new ArrayList<>();
  for (Integer i : list) {
    ret.add(i*2);
  }
  return ret;
}
doubleValues(Arrays.asList(1, 2, 3, 4, 5));

Kompliziert oder? In Scala geht es einfacher:

scala> val xs = (1 to 5).toList
xs: List[Int] = List(1, 2, 3, 4, 5)

scala> val doubleValues = for (x <- xs) yield x*2
doubleValues: List[Int] = List(2, 4, 6, 8, 10)

Zwei Zeilen Code dank Scalas `yield`. `yield` macht an sich nichts anderes als die berechneten Werte zurückzugeben. Da es nur in Verbindung mit `for` auftauchen kann, bedarf es auch sonst keiner weiteren Syntax um die Rückgabewerte direkt an eine Variable zu binden. Besonders toll ist auch, dass wir Range dazu nutzen können um uns eine List mit Zahlen zurückzugeben, in diesem Fall 1-5.

Anmerkung:
yield arbeitet nicht wie return. Es gibt eine Expression zurück und kein Statement. Folgendes Codestück würde also nicht funktionieren:

// does not work
for(i <- 0 to 10)
  if (i % 2 == 0) yield i
  else yield -i

Korrekt wäre dies hier:

for(i <- 0 to 10) yield
  if (i % 2 == 0) i
  else -i

Eine for-Expression kann nur ein einziges yield besitzen, welches dann aber auch immer einen Wert zurückgeben muss (falls keiner angegeben ist wird Unit zurückgegeben).

Indexbasierte Zugriffe

Javas foreach-Schleife hat den Nachteil, dass der Index des momentan benutzten Elementes nicht zur Verfügung steht. Benötigen wir diesen um z.B. ein Array aufzufüllen müssen wir wieder zur „normalen“ for-Schleife wechseln. Ein kurzes Codestück um zu verdeutlichen was ich meine:

int[] doubleValues(int[] ints) {
  int[] res = new int[ints.length];
  for (int i = 0; i < ints.length; i++) {
    res[i] = ints[i]*2;
  }
  return res;
}
doubleValues(new int[] {1, 2, 3, 4, 5});

Wenn wir in Scala die for-Expression mit Ranges verbinden, dann braucht uns auch das nicht stören:

scala> val xs = (1 to 5).toArray
xs: Array[Int] = Array(1, 2, 3, 4, 5)

scala> val doubleValues = for (i <- (0 until xs.length)) yield xs(i)*2
doubleValues: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10)

Einziger Wermutstropfen ist, dass der Typ von `doubleValues` Vector und nicht Array ist. Das kommt daher, weil die for-Expression den Typ der zu iterierenden Collection wählt, bei uns wäre das Range. Da Range aber nicht zur Verfügung steht um so `on the fly` aufgebaut zu werden wird innerhalb der Vererbungs-Hierarchie zum nächsthöheren Collection-Typ gegriffen, was die Standardimplementierung von IndexedSeq wäre. Wollen wir aber ein Array haben können wir es dies ganz einfach mit dem Aufruf von `toArray` auf doubleValues erzeugen.

Dank Scalas Typinferenz spielt es aber oft keine Rolle was für ein Typ die Collection hat mit der wir gerade arbeiten. Solange wir keine spezielle Eigenschaften der Implementierungen benötigen müssen wir die Typen nirgends angeben. Solange wir nur den statischen Ober-Typ einer Collection im Programm herumreichen (z.B. Seq) können uns die Runtime-Typen herzlich egal sein.

Auswahlen

Was machen wir wenn wir nur bestimmte Elemente aus einer Liste auswählen wollen? In Java würden wir eine if-Abfrage in die Schleife einbauen:

List<Integer> getEvenValues(List<Integer> list) {
  List<Integer> ret = new ArrayList<>();
  for (Integer i : list) {
    if (i%2 == 0) {
      ret.add(i);
    }
  }
  return ret;
}

Und in Scala? Da machen wir es genauso:

scala> val evenValues = for (x <- (0 to 10)) if (x%2 == 0) yield x
<console>:1: error: illegal start of simple expression
       val evenValues = for (x <- (0 to 10)) if (x%2 == 0) yield x
                                                           ^

Oder doch nicht. Was hat nicht funktioniert? Zu erst einmal zu letzterer Eingabe. Da wir kein `yield` werden auch keine Werte zurückgegeben. Das ist soweit logisch. Aber bei der ersten Eingabe nutzen wir es doch? Das Problem hier ist, dass in Scala alles immer einen Wert zurückgeben muss. Wir würden jetzt aber immer nur einen Wert zurückgeben wenn die Abfrage true ergeben würde. Wie lösen wir das Problem jetzt? Die erste Lösungsidee könnte sein, dass wir das yield einfach umdrehen, aber dann fehlt uns immer noch der else-Teil der if-Abfrage:

scala> val evenValues = for (x <- (0 to 10)) yield if (x%2 == 0) x
evenValues: scala.collection.immutable.IndexedSeq[AnyVal] = Vector(0, (), 2, (), 4, (), 6, (), 8, (), 10)

Bei allen ungeraden Zahlen gibt yield ein Unit zurück, repräsentiert durch die leeren Klammern. Wir haben jetzt also keine Seq aus Ints mehr, sondern eine Seq aus Ints und Units bzw. ihrem Obertyp AnyVal. Lösen können wir das Problem in dem wir die if-Abfrage in den Expression-Kopf verschieben:

scala> val evenValues = for (x <- (0 to 10) if x%2 == 0) yield x
evenValues: scala.collection.immutable.IndexedSeq[Int] = Vector(0, 2, 4, 6, 8, 10)

Warum funktioniert das jetzt? Die Antwort ist leider ein wenig komplexer und ich glaube wenn ich sie euch jetzt beantworten würde, würdet ihr es wahrscheinlich nicht vollständig verstehen. Ich bitte euch mir also zu verzeihen wenn ich die Antwort in ein späteres Kapitel verschiebe.
Lenkt eure Aufmerksamkeit lieber auf das if. Wie ihr sehen könnt fehlen die Runden klammern. Der Grund dafür ist, dass das kein wirkliches if-ist, sondern ein `filter`. Hm, jetzt hab ich euch z.T. ja doch eine Antwort auf die Frage gegeben warum der Code funktioniert. Nun gut, dann kann ich euch auch gleich eine klein wenig ausführlichere Antwort geben: Die for-Expression iteriert über eine Collection. Mittels `filter`, einer Operation auf Collections, werden nur die Objekte herausgesucht, auf die eine Bedingung zutrifft. Diese herausgefilterten Daten werden dann zurückgeben. Implementierungsdetails will ich euch aber jetzt nicht geben, die hebe ich mir auf später auf, ich muss ja noch mehr zum Schreiben haben. 😉

Für jetzt könnt ihr euch merken, dass ihr innerhalb der for-Expression die Klammern um das if immer weglassen dürft, sie sonst aber überall hinschreiben müsst (na gut, fast überall ;)).

for-Expressions in depth

for-Expressions werden euch als kompliziert erscheinen wenn wir uns ihre Implementierung anschauen. Dies wollen wir zum jetzigen Zeitpunkt aber noch nicht machen und uns stattdessen auf die leicht verständliche Syntax und auf all die Möglichkeiten konzentrieren, die uns die Expression bietet. Wir versuchen einmal mehrere Generatoren miteinander zu verschachteln:

val arr = Array(
  Array(1, 2, 3),
  Array(4, 5, 6),
  Array(7, 8, 9)
)
val values =
  for (x <- (0 until 3)) yield
    for (y <- (0 until 3)) yield
      arr(x)(y)*3

Wir erhalten als Ausgabe:

Vector(Vector(3, 6, 9), Vector(12, 15, 18), Vector(21, 24, 27))

Anmerkung:
Wir erhalten mehrdimensionale Arrays indem wir ein Array in einem Array erzeugen. Im Gegensatz zu Java oder C bietet uns Scala keine spezielle Syntax an mit der wir diese Erzeugung „verkürzen“ können. Der Grund dafür ist, dass dies nicht gewollt ist. Die Syntax von Scala soll möglichst allgemein gehalten sein, also ohne dass irgendwelche Ausnahmen in der Syntax vorhanden wären, die ansonsten nicht zum Erscheinungsbild der Sprache passen würden. Aus dem gleichen Grund werden Arrays in Scala mit runden und nicht mit eckigen Klammern adressiert. Runde Klammern hat man einfach überall, die eckigen existieren aber nur in Zusammenhang mit Arrays. Auf ineinander verschachtelte Collections können wir zugreifen indem wir uns ganz einfach die innere Collection holen und dann mit gleicher Syntax deren Elemente.

Das Prinzip entspricht einer verschachtelten for-Schleife in Java. Wir müssen nur schauen, dass die erste Expression die Werte der zweiten Expression mittels `yield` zurück gibt. Der Code hat nur zwei Mankos. Erstens schaut er nicht besonders schön aus, da er redundant ist (zwei Mal ein for und zwei Mal ein yield) und zweitens gibt er uns eine Collection in einer Collection zurück, vielleicht wollen wir aber nur eine Collection, die dafür alle Werte beinhaltet. Auch dafür bietet Scala eine Lösung:

scala> val values = for (x <- (0 until 3); y <- (0 until 3)) yield arr(x)(y)*3
values: scala.collection.immutable.IndexedSeq[Int] = Vector(3, 6, 9, 12, 15, 18, 21, 24, 27)

Es ist möglich mehrere Generatoren einfach hintereinander zu schreiben wenn man sie mit einem Strichpunkt trennt. Dadurch können wir geschachtelte Abarbeitungen unserer Collections erreichen. Natürlich ist es weiterhin möglich einen `filter` zu benutzen:

scala> val values = for (x <- (1 to 3); y <- (1 to 3) if x*y%2 == 0) yield x*y
values: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 2, 4, 6, 6)

Blöd nur, dass wir den Ausdruck `x*y` zwei Mal berechnen müssen. Da gibt es doch bestimmt auch eine Möglichkeit das zu umgehen? Natürlich!

scala> val values = for (x <- (1 to 3); y <- (1 to 3); z = x*y if z%2 == 0) yield z
values: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 2, 4, 6, 6)

Die Zuweisung innerhalb der for-Expression nennt man eine Definition. Diese erlaubt uns, uns Zwischenergebnisse zu „merken“. Das ist ein nützliches Feature wie man anhand des oberen Codes gut erkennen kann. So, jetzt wird es nur langsam ein wenig unübersichtlich. Wir haben einfach zu viel Information auf einer einzigen Zeile Code, teilen wir das also mal auf:

val values = for (
  x <- (1 to 3);
  y <- (1 to 3);
  z = x*y
  if z%2 == 0
) yield z

Sieht doch schon ein bisschen besser aus. Jetzt sind nur noch die Regeln für das Setzen des Strichpunktes ein bisschen verwirrend. Einmal braucht man es, einmal nicht. Die Regel ist eigentlich einfach, denn sie besagt, dass man den Strichpunkt immer benötigt wenn mehrere Anweisungen hintereinander stehen. Die einzige Ausnahme bei dieser Regel ist der filter, der mit einem if eingeleitet wird. Wir können also anstatt

scala> def x = {
     |   println("hello")
     |   5
     | }
x: Int

auch

scala> def x = { println("hello"); 5 }
x: Int

schreiben. Beim zweiten Beispiel müssen wir die Anweisungen mit einem Strichpunkt trennen, ansonsten kann der Compiler nicht erkennen wann die Anweisung zu Ende ist. Wenn wir die Anweisungen hingegen auf mehrere Zeilen auftrennen, dann hat der Compiler die Möglichkeit anhand des Zeilensprung-Zeichens das Ende einer Anweisung zu erkennen. Wieso geht das dann bei unserer for-Expression nicht? Die Antwort darauf ist eigentlich ganz einfach, wir müssen uns nur darauf besinnen was ich schon mal erklärt habe: Mehrere Anweisungen müssen innerhalb eines Blocks stehen. Und wie deklariert man einen Block? Genau, mit geschweiften Klammern.

val values = for {
  x <- (1 to 3)
  y <- (1 to 3)
  z = x*y
  if z%2 == 0
} yield z

Man kann mit for-Expressions noch ein bisschen tiefer in die Materie von Scala einsteigen. Die schon ein paar Mal angesprochene Implementierung der for-Expression ist z.B. etwas was man erst versteht wenn man schon sehr tief in Scala bzw. in der funktionalen Programmierung steckt. Irgendwann werdet ihr so weit sein, dass ich euch erklären kann was da intern so alles abläuft.

Advertisements

3 comments so far

  1. vanhelgen on

    Ich schätze mal die Schreibweise von der for-Comprehension ist von Haskell inspiriert, dort wurde es mir mit dem `ist Element aus` Operator (∈), wie in a ∈ {1, 2, 3} erklärt, den das <- nachahmen soll, was ich recht anschaulich finde. Dachte das hilft beim Merken 🙂

  2. Taig on

    „Es ist möglich mehrere Generatoren einfach hintereinander zu schreiben wenn man sie mit einem Doppelpunkt trennt.“

    Da ist sicherlich das Semikolon gemeint, oder?

    • Simon Schäfer on

      Ja, ich habe einen Strichpunkt gemeint. Vielen Dank für den Hinweis.


Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: