Archive for 28. Juli 2011|Daily archive page
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 -iKorrekt wäre dies hier:
for(i <- 0 to 10) yield if (i % 2 == 0) i else -iEine 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.
REPL transcript mode
Seit Scala 2.9.1 ist mir der transcript mode der REPL erst so richtig bewusst geworden:
scala> scala> 4+1 // Detected repl transcript paste: ctrl-D to finish. // Replaying 1 commands from transcript. scala> 4+1 res48: Int = 5
In den früheren Versionen hat er zwar schon existiert, da bei Eingabe von `scala>` aber kein Kommentar erschien hab ich gedacht die REPL hätte sich aufgehängt. Auf die Idee, dass man den Modus mit STRG+D beenden kann bin ich nicht gekommen.
Der transcript mode ist besonders nützlich wenn man REPL output irgendwo gefunden hat (z.B. in diesem Blog), aber nicht jeden Befehl von Hand eingeben möchte.
Ein Beispiel:
scala> val xs = List(1, 2, 3)
xs: List[Int] = List(1, 2, 3)
scala> xs map { _+1 }
res51: List[Int] = List(2, 3, 4)
scala> xs filter { _ < 3 }
res52: List[Int] = List(1, 2)
scala> xs.size
res53: Int = 3
Einfach den kompletten Inhalt in die REPL kopieren und mit STRG+D parsen lassen.
Teil 6.3: Typkonvertierungen
Es kommt oft vor, dass wir einen Typ haben aber für eine bestimmte Operation einen ganz anderen brauchen. So erwartet eine Methode z.B. einen Double-Wert, wir haben aber nur einen Int. In diesem Fall wäre es kein Problem, da Scala sogenannte `widening-operations` besitzt. D.h. alle Zahlentypen werden automatisch in einen Typ konvertiert, der den Zahlentyp aufnehmen kann.
Die Wichtigsten davon sind:
Int -> Long -> Float -> Double
Dass das funktioniert sehen wir hier:
scala> val l: Long = 12345 l: Long = 12345 scala> val f: Float = l f: Float = 12345.0 scala> val d: Double = f d: Double = 12345.0
Wenn wir mit anderen Typen arbeiten finden keine solchen automatischen Konvertierungen statt, wir müssen sie also von Hand vornehmen. Das geht in Scala aber denkbar einfach. Alle Konvertierungsmethoden fangen mit `to` an und enden mit dem Typ in den sie konvertiert werden sollen:
scala> val xs = Seq(1, 2, 3) xs: Seq[Int] = List(1, 2, 3) scala> xs.toList res22: List[Int] = List(1, 2, 3) scala> xs.toSet res23: scala.collection.immutable.Set[Int] = Set(1, 2, 3) scala> xs.toString res24: String = List(1, 2, 3)
Natürlich können wir nicht jeden Typ in jeden konvertieren. Es ist z.B. nicht möglich eine Seq mit Int in eine Map zu wandeln. Was soll auch das Ergebnis sein? Die Map benötigt sowohl einen Schlüssel als auch einen dazugehörenden Wert:
scala> xs.toMap
<console>:10: error: Cannot prove that Int <:< (T, U).
xs.toMap
^
scala> val xs = Seq(1 -> "a", 2 -> "b", 3 -> "c")
xs: Seq[(Int, java.lang.String)] = List((1,a), (2,b), (3,c))
scala> xs.toMap
res26: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> a, 2 -> b, 3 -> c)
Falls unsere Seq aber schon einen Tuple2 beinhaltet ist die Konvertierung gar kein Problem.
Neben den `to`-Methoden besteht auch noch die Möglichkeit mit Hilfe der `++`-Methode die Elemente einfach an eine andere Collection dranzuhängen:
scala> val xs = Seq(1 -> "a", 2 -> "b", 3 -> "c") xs: Seq[(Int, java.lang.String)] = List((1,a), (2,b), (3,c)) scala> Set.empty ++ xs res29: scala.collection.immutable.Set[(Int, java.lang.String)] = Set((1,a), (2,b), (3,c)) scala> Nil ++ xs res30: List[(Int, java.lang.String)] = List((1,a), (2,b), (3,c)) scala> Map.empty ++ xs res31: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> a, 2 -> b, 3 -> c)
Die Methode `empty` erzeugt eine leere Collection, an die die Elemente angefügt werden können. Anstatt `empty` könnten wir auch einfach einen leeren Konstruktor aufrufen (z.B. `Set() ++ xs`). Wir müssen aber keinen leeren Konstruktor aufrufen sondern können ihm direkt unsere Collection übergeben:
scala> Set(xs) res33: scala.collection.immutable.Set[Seq[(Int, java.lang.String)]] = Set(List((1,a), (2,b), (3,c))) scala> Set(xs: _*) res34: scala.collection.immutable.Set[(Int, java.lang.String)] = Set((1,a), (2,b), (3,c))
Wir erhalten zwei verschiedene Ausgaben. Beim ersten Mal erhalten wir ein Set, das eine List mit Tuple2 beinhaltet. Beim zweiten Mal erhalten wir ein Set, das die Tuple2 ohne eine weitere List beinhaltet. Das letztere Ergebnis ist das das wir haben wollen, aber was ist der Unterschied?
Nun, der Konstruktor von Set erwartet ein Objekt, das er in das Set einfügen soll. Er kann ja nicht wissen ob wir die Seq oder die Tuple2 aufnehmen wollen. Wir müssen es ihm also irgendwie mitteilen.
Dies erreichen wir mit dem am Anfang etwas ungewöhnlich ausschauenden `: _*`. Die Bedeutung des Punktes kennen wir bereits. Damit wird ein Typ an eine Variable gebunden. Die beiden anderen Zeichen sind uns dagegen noch unbekannt. Der Stern kennzeichnet in Scala `varargs`. Wir können damit festlegen, dass beliebig viele Argumente an eine Variable gebunden werden können:
scala> def deliverVarArgs(i: Int*) { println(i) }
deliverVarArgs: (i: Int*)Unit
scala> deliverVarArgs(1, 2, 3, 4)
WrappedArray(1, 2, 3, 4)
Der Compiler erzeugt für uns ein WrappedArray[Int], das an die Variable `i` gebunden wird.
Das letzte unbekannte Symbol ist der Unterstrich. Dieser kann in Scala viele Bedeutungen haben. Die Wichtigste ist, dass er einen Platzhalter bzw. ein Wildcard-Symbol darstellt. In Verbindung mit dem Stern bedeutet es wandle etwas in ein varargs-Argument um. Das „etwas“ ist in diesem Fall die Variable `xs`. Wir können damit also angeben ob die Elemente der Seq oder die Seq selbst an den Konstruktor übergeben werden.
Zusammenfassend haben wir jetzt also drei Möglichkeiten kennen gelernt eine Collection in eine andere zu wandeln:
scala> Set.empty ++ xs res38: scala.collection.immutable.Set[(Int, java.lang.String)] = Set((1,a), (2,b), (3,c)) scala> Set(xs: _*) res39: scala.collection.immutable.Set[(Int, java.lang.String)] = Set((1,a), (2,b), (3,c)) scala> xs.toSet res40: scala.collection.immutable.Set[(Int, java.lang.String)] = Set((1,a), (2,b), (3,c))
Welche ihr anwenden wollt bleibt vollkommen euch überlassen. Es gibt sogar noch weitere Möglichkeiten, da diese aber nur in Spezialfällen angewendet werden werde ich erst später darauf eingehen.
Casts
Manchmal wollen wir aber gar nicht konvertieren, sondern casten. Wir haben z.B. eine Implementierung und wollen aber nur mit der Schnittstelle arbeiten. Oder aber es ist genau anders herum – wir haben die Schnittstelle wollen aber an die Implementierung. In Scala gibt es dafür `asInstanceOf`:
scala> val xs = List(1, 2, 3) xs: List[Int] = List(1, 2, 3) scala> xs.asInstanceOf[Seq[Int]] res42: Seq[Int] = List(1, 2, 3)
Bein Upcasten ist es aber auch ausreichend einer Variable einfach einen höheren Typ zu geben:
scala> val seq: Seq[Int] = xs seq: Seq[Int] = List(1, 2, 3)
Beim Downcasten müssen wir aufpassen ob der Cast zur Laufzeit auch funktionieren wird. Prüfen können wir dies mit `isInstanceOf`:
scala> val xs = Seq(1, 2, 3)
xs: Seq[Int] = List(1, 2, 3)
scala> xs.isInstanceOf[List[Int]]
<console>:9: warning: non variable type-argument Int in type List[Int] is unchecked since it is eliminated by erasure
xs.isInstanceOf[List[Int]]
^
res0: Boolean = true
scala> xs.asInstanceOf[List[Int]]
res1: List[Int] = List(1, 2, 3)
Scala unterliegt leider den gleichen Einschränkungen was Generics betrifft wie Java auch: type erasure. Die Typprüfung kann also nicht prüfen ob die Liste tatsächlich aus lauter Ints besteht, wir sollten also gar nicht erst auf einen genauen Typ prüfen:
scala> xs.isInstanceOf[List[String]]
<console>:9: warning: non variable type-argument String in type List[String] is unchecked since it is eliminated by erasure
xs.isInstanceOf[List[String]]
^
res3: Boolean = true
scala> xs.isInstanceOf[List[_]]
res4: Boolean = true
Der Unterstrich steht hier wieder für einen unbekannten Typ oder ein Wildcard. Durch den Cast können wir den Typinferenz-Checker überlisten und ihm falsche Daten vorgaukeln. Versucht also wenn es nur irgendwie möglich ist einen parametisierten Cast zu vermeiden:
scala> val ys: List[String] = xs.asInstanceOf[List[String]] // don't do that! ys: List[String] = List(1, 2, 3) scala> val ys: List[Any] = xs.asInstanceOf[List[Any]] // ok ys: List[Any] = List(1, 2, 3)
Any entspricht dem Object von Java. Es ist der Obertyp aller Objekte in Scala, in ihn kann also immer gecastet werden. Grundsätzlich sollte man Casts aber immer vermeiden, da sie fehleranfällig sind. Es gibt in Scala andere, bessere Wege um auf korrekte Typen zu überprüfen. Von diesen werdet ihr noch früh genug etwas hören. Zum Schluss noch eine Graphik, die Scalas wichtigste Typen auflistet:

Teil 6.2: Set, Tuple und Map
Sequenzen von Daten reichen für manche Anwendungsfälle nicht aus. Wenn wir Beispielsweise Daten sortiert halten wollen ist eine Seq nicht die beste Wahl, da das sortierte Einfügen ein Flaschenhals darstellen kann. Das gleiche gilt wenn wir Daten suchen wollen. Die Seq zu durchlaufen und jedes Element zu überprüfen ob es auf unsere Anforderungen passt kann bei vielen Elementen viel zu lange dauern. Aber dafür haben wir ja Sets und Maps, die dafür genau das Richtige sind.
Da beide Collection-Typen vom gleichen Obertyp erben wie Seq können wir auf die gleichen Methoden zurückgreifen um die Datenstrukturen anzusprechen.
Set
Testen wir die Methoden von Set doch gleich einmal:
scala> val set = Set(1, 2, 3) set: scala.collection.immutable.Set[Int] = Set(1, 2, 3) scala> set contains 3 res116: Boolean = true scala> set contains 5 res117: Boolean = false scala> set ++ List(3, 4, 5) res118: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)
Wie zu erkennen speichert das Set nur Unikate. Wenn wir also eine Liste aus Daten haben und nur die Anzahl der verschiedenen Elemente haben wollen genügt es diese einfach in ein Set einzufügen und dann die Größe des Sets zu ermitteln:
scala> val xs = List(2, 5, 1, 7, 3, 35, 72, 23, 6, 1, 73, 35, 76, 86, 17) xs: List[Int] = List(2, 5, 1, 7, 3, 35, 72, 23, 6, 1, 73, 35, 76, 86, 17) scala> val set = Set() ++ xs set: scala.collection.immutable.Set[Int] = Set(5, 1, 6, 73, 2, 17, 86, 76, 7, 3, 35, 72, 23) scala> set.size res119: Int = 13
Ein weiteres Merkmal ist, dass die Elemente im Set nicht sortiert sind, wie wir an der String-Repräsentation erkennen können. Um dies zu ändern müssen wir anstatt eines Sets einfach ein SortedSet referenzieren:
scala> import scala.collection.immutable.SortedSet import scala.collection.immutable.SortedSet scala> val set = SortedSet[Int]() ++ xs set: scala.collection.immutable.SortedSet[Int] = TreeSet(1, 2, 3, 5, 6, 7, 17, 23, 35, 72, 73, 76, 86)
Es gibt in scala.Predef keine Typreimplementierung von SortedSet weshalb wir es von Hand importieren müssen. Dies erreichen wir ganz einfach mit dem import-Statement. Bitte beachtet, dass wir das SortedSet von Hand typisieren müssen, die die Typinferenz des Compilers hier nicht mehr ausreicht.
Tuple
Der Konstruktor von Map erwartet sogenannte Tuple von Daten. Um also eine Map zu erstellen müssen wir zuerst einen Tuple erstellen. Aber was ist ein Tupel? Ein Tupel ist eine Datenstruktur, die beliebige Daten aufnehmen und mit einander „verbinden“ kann. Die einfachste Möglichkeit einen Tuple zu kreieren ist einfach irgendwelche Daten mit einem Komma voneinander zu trennen und diese dann einzuklammern:
scala> ("de", "Germany")
res125: (java.lang.String, java.lang.String) = (de,Germany)
scala> (1, "hello", 5.34)
res126: (Int, java.lang.String, Double) = (1,hello,5.34)
In Scala können bis zu 22 solcher Daten „vertupelt“ werden. Tuple stellt aber keine Collection dar. Die Methoden, die wir bisher kennen gelernt haben sind größtenteils deshalb auch nicht darauf anzuwenden. Ein Tuple soll auch nicht als Collection dienen. Statt dessen soll er die Möglichkeit bieten mehrere zusammenhänge Daten innerhalb des Codes hin und her zu reichen, ohne dass man sich dafür eine eigene Datenstruktur bauen muss. Brauchen können wir dies bspw. bei einer Methode, die mehrere Daten zurückgeben soll:
def heavyCalculation(): (Int, Int) = {
val memoryUsage = 30
val cpuUsage = 91
(cpuUsage, memoryUsage)
}
Stellen wir uns vor, dass diese Methode große Berechnungen machen müsste. Nachdem die Berechnungen abgeschlossen sind würden wir gerne Wissen wie viel Systemressourcen sie benötigt haben. Wir geben also die Speicher- und CPU-Auslastung in Prozent zurück. Der Rückgabewert wird ebenfalls in Klammern geschrieben um dem Compiler zu verdeutlichen, dass ein Tuple zurückgegeben werden soll.
Tuple haben nur einen kleinen Schönheitsfehler wenn man versucht auf ihre Elemente zuzugreifen:
scala> heavyCalculation() res130: (Int, Int) = (91,30) scala> res130._1 res131: Int = 91 scala> res130._2 res132: Int = 30
Da ein Tuple beliebige Daten aufnehmen kann gab es bei ihrer Implementierung keine Möglichkeit einen geeigneten Namen zu definieren der zu jedem Rückgabewert passt. Man hat sich deshalb auf die Schreibweise tuple._N entschieden wobei N ein Wert zwischen 1 und 22 ist. Je nach Größe des Tuples kann man dann damit an dessen Elemente gelangen. Wird ein Feld adressiert, das bei der momentanen Größe des Tuples nicht existieren kann bekommen wir eine Fehlermeldung:
scala> res130._3
<console>:11: error: value _3 is not a member of (Int, Int)
res130._3
^
Wenn ein Tuple die Größe 2 hat wird auch von einem Tuple2 gesprochen, bei der Größe 3 von einem Tuple3 usw. Um einen Tuple2 zu erstellen gibt es noch eine spezielle Methode:
scala> 1 -> "hello"
res141: (Int, java.lang.String) = (1,hello)
scala> (1).->("hello")
res142: (Int, java.lang.String) = (1,hello)
Dies ist syntaktischer Zucker für die wohl am meisten erstellte Tuple-Art. Vor allem in Verbindung mit Maps werden wir regen Gebrauch davon machen. Um an alle Elemente eines Tuples zu kommen bieten sich die Method `productIterator` an:
scala> val t = (6, "hello", 3.245, true, 'h') t: (Int, java.lang.String, Double, Boolean, Char) = (6,hello,3.245,true,h) scala> val i = t.productIterator i: Iterator[Any] = non-empty iterator scala> while (i.hasNext) println(i.next) 6 hello 3.245 true h
Wir erhalten einen Iterator, über den wir nach Lust und Laune iterieren können.
Map
Maps besitzen die tolle Eigenschaft, dass sie zu einem bestimmten Schlüssel einen Wert abspeichern. Das könnte z.B. so aussehen:
scala> val m = Map("de" -> "Germany", "en" -> "England", "fr" -> "France")
m: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(de -> Germany, en -> England, fr -> France)
Um einen Wert aus einer Map zu erhalten gibt es zwei Möglichkeiten:
scala> m("de")
res144: java.lang.String = Germany
scala> m.get("de")
res145: Option[java.lang.String] = Some(Germany)
Die erste Schreibweise kennen wir bereits. Die Zweite ist auch nicht viel komplizierter gibt aber einen komischen Typ namens Some zurück. Some ist ein Untertyp vom Typ Option. Wofür dieser gut ist sehen wir wenn wir einen Wert anfordern der nicht existiert.
scala> m("ch")
java.util.NoSuchElementException: key not found: ch
<stacktrace>
scala> m.get("ch")
res147: Option[java.lang.String] = None
Im ersten Fall erhalten wir eine Exception, die wir natürlich nicht erhalten wollen. Die Vorgehensweise in Java wäre jetzt zuerst zu prüfen ob der Schlüssel existiert und nur dann seinen Wert anzufordern. Es würde auch die Möglichkeit bestehen einfach die Exception abzufangen und zu verarbeiten. Die Vorgehensweisen haben aber ein Problem: Sie erfordern viel unnützen Code, der uns nicht direkt bei der Bewältigung unserer Aufgabe weiterbringt. Hier eine Beispielimplementierung der ersten Vorgehensweise:
def mapValue[A, B](m: Map[A, B], s: A) = if (m contains s) m(s) else null scala> mapValue(m, "de") res151: String = Germany scala> mapValue(m, "ch") res152: String = null
Die erste Implementierung ist an sich sehr kurz, wir müssen aber null zurückgeben wenn der Wert nicht existiert. Der große Nachteil des Codes ist weiterhin, dass er nicht generisch ist. Würden wir das auch noch einbauen wollen müssten wir nochmal ein bisschen mehr schreiben. Die Implementierung mit Exceptions würde ich gerne auslassen, da sie wohl sowieso niemand praktisch anwendet. Sie würde sowieso mehr Codezeilen beanspruchen.
Scala macht es sich da einfacher und gibt ein Option zurück, das entweder Some oder None sein kann, je nach dem ob ein Wert existiert oder nicht. Was man mit Option so alles machen und wie es uns vor NullPointerExceptions beschützen kann möchte ich nochmal gesondert besprechen, das soll nicht Inhalt dieses Artikels sein.
Um an die Schlüssel oder Werte einer Map zu gelangen werden uns die folgenden Methoden bereitgestellt:
scala> m.keys res158: Iterable[java.lang.String] = Set(de, en, fr) scala> m.values res159: Iterable[java.lang.String] = MapLike(Germany, England, France)
Mehr brauchen wir momentan eigentlich nicht über Map zu wissen.
Comments (3)