for
Ausdrücke
In [SQ1] beschreiben Martin Odersky et al. den Scala
for
Ausdruck als Scala's Schweizer Armeemesser der Iteration.
"Scala's for expression is a Swiss army knife of Iteration."
for
Ausdruck? Warum nicht for
Schleife? Hier sei angemerkt, dass in Scala
ein sogenannter for
Ausdruck keine Schleife im ursprünglichen Sinn darstellt.
In Scala iterieren wir über eine Menge vorgegebener Daten. Diese Daten müssen nicht einmal
einer Collection enstammen, sondern der Typ, über den wir iterieren, muss nur ein paar bestimmte
höhere Funktionen implementieren, worauf wir aber erst später einghen wollen.
Im Bezug zu Scala's for
finden wir in der englischsprachigen Literatur immer wieder die Bezeichnungen
for - expression und for - comprehension, die darauf hindeuten, dass Scala's for
Ausdruck
keine eigentliche Schleife ist. Wenn es aber erlaubt ist das englische Wort loop
mit Schleife zu übersetzen, so kann man es uns nicht übel nehmen, wenn wir im deutschsprachigen
Raum von for
Schleifen sprechen. Wir befinden uns damit auch in guter Gesellschaft, da unter anderen
in [SQ1] (Odersky et. al. - Programming in Scala) mehrfach von for loops
die Rede ist (z.B. S.48).
Gerade Umsteiger aus imperativen oder objektorientierten Sprachen, wie Java, können sich den
for
Ausdruck als for
Schleife vorstellen. Die grundlegenden Funktionen wirken
wie eine Schleife. Aber: Scala's for
Ausdruck kann mehr als Int
Werte zu durchlaufen.
Der for
- Ausdruck wird oft dazu verwendet
eine Int - Variable einen bestimmten Wertebereich durchlaufen zu lassen.
Das nachfolgende Beispiel zeigt einen for
- Ausdruck, wo eine Variable
i die Werte 0 bis 4 annimmt und der aktuelle Wert ausgegeben wird.
for (i <- 0 to 4) println(i)
Der Ablauf dieses Ausdrucks führt zu folgender Ausgabe:
0 1 2 3 4
Möchte man, dass der letzte Wert nicht ausgegeben wird,
kann man folgende Variante des for
- Ausdrucks verwenden:
for (i <- 0 until 4) println(i)
Der Ablauf dieses Ausdrucks führt zu folgender Ausgabe:
0 1 2 3
Die oben vorgestellten for
- Ausdrücke können den Eindruck
hinterlassen, dass es sich bei der Variable i
um eine var
Variable handelt, da der Wert bei jedem Schleifendurchlauf verändert wird.
Dies ist jedoch nicht der Fall. Bei jedem Schleifendurchlauf wird eine
neue val
Variable für i
verwendet. Eine
Zuweisung eines neuen Wertes für i
ist innerhalb des
des Auswertungsrumpfes nicht möglich.
Auch wenn der for
Ausdruck nicht auf klassische Collections beschränkt ist,
wollen wir zunächst der einfachheit halber, davon ausgehen.
<-
im hier gezeigten for
- Ausdruck
wird als Generator (eng. generator) bezeichnet.
<-
generiert individuelle Werte aus einer Collection
zur Verwendung in einem Ausdruck 1.
Rechts von <-
steht die Collection, aus der die Werte
entnommen werden sollen. Links von <-
steht der Name einer
val
Variablen, auf die wir innerhalb des Auswertungsrumpfes zugreifen können.
Wenn also <-
Elemente aus einer Collection generiert,
müssen auch die Ausdrücke 0 to 4
und 0 until 4
als Ergebnis eine Collection liefern, was auch der Fall ist. Die beiden Ausdrücke liefern
eine Collection vom Typ scala.collection.immutable.Range
(bzw.
scala.collection.immutable.Range.Inclusive
bei to
),
welche Int-
Elemente in einem Bereich zwischen einem Start- und Endwert
bereitstellt. Der Umstand, dass eine Collection im for
Ausdruck
verwendet wird, führt dazu, dass wir die Collection auch außerhalb
des for
Ausdruckes definieren können und mithilfe einer Variablen
dem for
Ausdruck zuführen können.
scala> val myRange = 1 to 3 myRange: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3) scala> for (i <- myRange) println(i) 1 2 3
In den bisherigen Beispielen haben wir Int
Literale verwendet. Wenn
wir uns die API Dokumentation ansehen, werden wir feststellen, dass für
Int
die Funktionen to
und until
nicht definiert sind.
Damit die Funktionen zur Verfügung stehen, wird der Int
Wert
zunächst (durch impliziete Typumwandlung) in einen scala.runtime.RichInt
Typ umgewandelt, für den die Funktionen definiert sind. Die impliziete Typkonvertierung
ist in scala.LowPriorityImplicits
definiert, welche durch scala.Prefed
erweitert wird.
In den for
- Ausdrücken, die wir bisher kennengelernt haben, muss nicht
unbedingt eine Collection vom Typ Range
verwendet werden. Wir können auch
problemlos dem Generator eine Collection vom Typ List
zur Verarbeitung
übergeben.
scala> val myList = List(1,2,3) myList: List[Int] = List(1, 2, 3) scala> for (i <- myList) println(i) 1 2 3
In den bisherigen for
Ausdrücken sind wir Int
- Werte immer mit einem Inkrement von 1 durchlaufen.
Die beiden Methoden to
und until
sind in RichInt
überladen,
sodass wir das Inkrement festlegen können.
def to (end: Int, step: Int): Inclusive def to (end: Int): Inclusive def until (end: Int, step: Int): Range def until (end: Int): Range
Das nachfolgende Beispiel zeigt einen for
Ausdruck, welcher Int
- Werte mit einem Inkrement von 3
durchläuft.
scala> for (i <- 4 to (17,3)) println(i) 4 7 10 13 16
Die hier gezeigte Vorgehensweise gilt Analog für die Methode until
.
Die Int
- Werte müssen nicht in aufsteigender Reihenfolge durchlaufen werden,
vielmehr können wir auch ein Dekrement (die Werte werden kleiner) als Schrittweite festlegen.
scala> for (i <- 13 until (6,-2)) println(i) 13 11 9 7
Bisher haben wir nur for
-Ausdrücke mit Int
-Werten verwendet.
Wir können jedoch auch Collections anderer (aller) Typen verwenden.
Beispielhaft folgt nachfolgend eine Anwendung der for
Ausdrucks
auf eine List
mit Strings.
scala> val myList = List("Hello","world","Scala","rocks") myList: List[java.lang.String] = List(Hello, world, Scala, rocks) scala> for (s <- myList) println(s) Hello world Scala rocks
In vielen Fällen möchten wir verschieden Werte verschachtelt durchlaufen.
In Scala können wir die Verschachtelung in einer for
Anweisung definieren.
Beispielsweise können wir zwei Generatoren in einem for
Ausdruck definieren,
die dann verschachtelt durchlaufen werden.
scala> for (i <- 0 to 2; j <- 3 to 4) println(i+" "+j) 0 3 0 4 1 3 1 4 2 3 2 4
Verwenden wir statt der runden Klammern geschweifte Klammern und definieren die Generatoren in zwei Zeilen, können wir auf die Trennung mit einem Semikolon verzichten.
scala> for {i <- 0 to 2 | j <- 3 to 4} println(i+" "+j) 0 3 0 4 1 3 1 4 2 3 2 4
Nicht immer möchten wir alle Werte einer Collection in einem for
Ausdruck
verarbeiten. Zu diesem Zwecke stehen uns im for
Ausdruck sogenannte
Filter zur Verfügung. Ein Filter wird dabei einfach durch einen if
-Ausdruck
innerhalb des for
Ausdrucks definiert. Liefert der Filter das Ergebnis true
wird der aktuelle Schleifendurchlauf bearbeitet, liefert der Filter false
wird
der aktuelle Schleifendurchlauf nicht bearbeitet.
Im nachfolgenden Beispiel wenden wir einen for
Ausdruck auf einen Range
mit den Werten 1 bis 10 (inklusive) an und geben nur die geraden Werte auf der Systemausgabe aus.
object ForLoop { def main(args: Array[String]): Unit = { for {i <- 1 to 10 if (i % 2 == 0)} println(i) } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
2 4 6 8 10
Zugegeben, das gleiche hätten wir auch mit einer entsprechenden Range
-Definition
erreichen können, was aber nicht Inhalt des Abschnittes ist.
Wie bei den Generatoren sind wir bei Filter nicht auf einen einzigen if
Ausdruck beschränkt
und können mehrere Filter definieren. Im nachfolgenden Beispiel verwenden wir zwei Generatoren
und zwei Filter. Zur Ausführung des Schleifeninhaltes kommt es im Beispiel nur, wenn die
Variable i
gerade und die Variable j
ungerade ist.
object ForLoop2 { def main(args: Array[String]): Unit = { for {i <- 1 to 6 if (i % 2 == 0) j <- 1 to 6 if (j % 2 != 0)} println(i+" "+j) } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
2 1 2 3 2 5 4 1 4 3 4 5 6 1 6 3 6 5
Die Filter haben keine bestimmte Position innerhalb des for
-Ausdrucks.
Greifen Filter aber auf Variable zu, die im for
-Ausdruck definiert werden,
müssen diese nach der Variablendefinition positioniert werden. Dementsprechend
hätten wir unser obiges Beispiel auch folgendermaßen schreiben können.
object ForLoop2 { def main(args: Array[String]): Unit = { for {i <- 1 to 6 j <- 1 to 6 if (i % 2 == 0) if (j % 2 != 0)} println(i+" "+j) } }
Bis jetzt haben wir mit unseren for-Ausdrücken nur Seiteneffekte erzielt. In Scala wird jedoch propagiert, wenn möglich, die funktionale Programmierung zu bevorzugen. Seiteneffekte wiedersprechen der funktionalen Programmierung, sodass wir das Ergebnis der for-Ausdrücke auswerten müssen, um diese sinnvoll in der funktionalen Programmierung verwenden zu können.
Sehen wir uns zunächst das Ergebnis eines for-Ausdrucks, in der Form wie wir ihn bis jetzt verwendet haben, an.
object ForResult1 { def main(args: Array[String]): Unit = { val list: List[Int] = List(1,2,3,4) val result = for (i <- list) i * 2 println(result) } }
Als Ergebnis liefert uns dieses Programm ()
, also Unit
. Mit diesem Ergebnis eines for-Ausdrucks
können wir wenig anfangen. Abhilfe schafft hier das Schlüsselwort yield
, welches dazu dient,
die Ergebnisse der einzelnen Durchläufe "einzufangen".
object ForResult2 { def main(args: Array[String]): Unit = { val list: List[Int] = List(1,2,3,4) val result = for (i <- list) yield i * 2 println(result) } }
Als Ergebnis dieses for-Ausdrucks erhalten wir List(2, 4, 6, 8)
. Mithilfe von yield
wird jedes einzelne
Ergebnis des for-Ausdrucks in Form einer neuen Collection als Ergebnis des gesamten for-Ausdrucks zusammengefasst.