Für eine (objekt-)funktionale Sprache mit ausgefeiltem Typsystem ist eine umfangreiche Collection Library einfach ein Muss. Daher leuchtet ein, dass im Lauf der Zeit - und Scala durfte ja schon seit 2003 reifen - eine “gewachsene” Struktur entsteht, die in Sachen Eleganz, Durchgängigkeit und Erweiterbarkeit die eine oder andere Schwachstelle offenbart. Diese zu beseitigen haben sich die Scala-Schöpfer für die Version 2.8 auf die Fahne geschrieben. Offenbar sehr viel Arbeit, wie man Kommentaren auf der scala-internals Mailing-Liste entnehmen kann.
Werfen wir zunächst einen Blick auf die “Organisationsstruktur” der überarbeiteten Collections. Waren in Scala 2.7 einzelne Typen noch über verschiedene unzusammenhängende Packages verstreut, z.B. List im Package scala und Set im Package scala.collection, so finden wir in Scala 2.8 wirklich alle Collection-Typen sauber im Package scala.collection bzw. dort in Sub-Packages aufgeräumt:
- scala.collection enthält Basis-Typen
- scala.collection.immutable enthält unveränderliche Collections
- scala.collection.mutable enthält veränderliche Collections
- scala.collection.generic enthält Bausteine für die Implementierung
Als Nutzer werden wir vor allem mit den unveränderlichen Collections aus scala.collection.immutable und den veränderlichen aus scala.collection.mutable zu tun haben. Abbildung 1 zeigt die ersten Ebenen der Vererbungshierarchie.
Neu hinzu kommt in Scala 2.8 Traversable als Wurzel aller Collections: Dieser Trait implementiert zahlreiche Methoden, die allen Collections gemeinsam sind, u.a. die typischen Verdächtigen wie map, flatMap und filter. Einzig foreach ist abstrakt. Bemerkenswert ist hierbei, dass alle Collections das durch Traversable definierte API in Form Ihrer eigenen Typen darstellen: Traversable.filter ergibt ein Traversable, wohingegen List.filter eine List ergibt.
Typ-Konsistenz
Dieses Verhalten, dass die gemeinsamen Operationen die “richtigen” Typen zurückgeben, ist für uns als Anwender natürlich eine wichtige Voraussetzung. Doch wie kann das erreicht werden? Entweder werden die Operationen auf jedem Collection-Typ individuell implementiert, was zu massiven Redundanzen und damit verbundenen Qualitätsrisiken führt. Oder man lässt sich eine Lösung einfallen, um Code nicht duplizieren zu müssen. Die Scala-Entwickler entschieden sich für Letzteres, indem sie das Vermeiden von Code-Duplizierung zu einem der obersten Design-Ziele erhoben. Als Resultat finden wir alle gemeinsamen Operationen in Collection-Templates aus dem Package scala.collection.generic.
Wer einen Blick in den Source-Code wagt, wird feststellen, dass die Scala-Entwickler tief in die Trickkiste greifen mussten, um alle Anforderungen mittels geeigneter Abstraktionen bzw. Indirektionen umsetzen zu können. So finden wir nun z.B. Builders, BuilderFactorys und implizite Parameter; allesamt sehr lehrreich in Hinblick auf die Möglichkeiten, die Scala bietet. Und wenn uns die existierenden Collection-Typen einmal nicht ausreichen sollten, dann können wir dank der hervorragenden Ausgangsbasis recht einfach neue Typen selber schreiben.
Aber warum soll das Ganze eigentlich so kompliziert sein? Das beantworten wir am besten mit einem kleinen Beispiel: Auf einen String bzw. einen RichString sollen mittels map unterschiedliche Funktionen angewendet werden.
Zunächst einmal sollen alle Buchstaben in Großbuchstaben verwandelt werden. Als Resultat erwarten wir natürlich wieder einen String:
scala> "abc" map { x => x.toUpper }res0: String = ABC
Nun wollen wir einfach die numerische Entsprechung der Buchstaben für eine Addition verwenden, sodass wir eine Collection mit Int-Elementen als Resultat erwarten:
scala> "abc" map { x => x + 1 }res1: ...IndexedSeq[Int] = IndexedSeq(98, 99, 100)
Was seit Scala 2.8 so wunderbar intuitiv funktioniert - mit Scala 2.7 ergibt das erste Beispiel übrigens keinen String, sondern einen ArrayBuffer! - ist in der Tat nicht trivial: Je nachdem, wie die Funktion aussieht, die wir map übergeben, können sich sowohl der Typ der Collection, als auch der Typ der Elemente ändern.
Collections erzeugen
Ein weiterer Aspekt, in dem die Collections von Scala 2.8 ein einheitliches Interface bieten, ist das Erzeugen von neuen Collection-Objekten. Einige Beispiele:
scala> Traversable(1, 2, 3)res0: Traversable[Int] = List(1, 2, 3)scala> Iterable("a", "b", "c")res1: Iterable[java.lang.String] = List(a, b, c)scala> Seq("x", "y", "z")res2: Seq[java.lang.String] = List(x, y, z)scala> IndexedSeq("x", "y", "z")res3: IndexedSeq[java.lang.String] = IndexedSeq(x, y, z)scala> Set(1, 2, 3)res4: ...Set[Int] = Set(1, 2, 3)scala> Map(1 -> "a", 2 -> "b")res5: ...Map[Int,java.lang.String] = Map(1 -> a, 2 -> b)
In Scala 2.7 können wir zwar auch viele Collection-Typen auf diese Weise erzeugen, aber eben nicht alle. Eine weiter Neuerung von Scala 2.8 ist, dass beliebige leere Collections mittels empty erzeugt werden können, z.B.:
scala> List.emptyres6: List[Nothing] = List()
Wichtig für den Umstieg
Einige wichtige Collection-Typen, z.B. List, befinden sich in Scala 2.8 in anderen Packages als zuvor. Um den Umstieg von Scala 2.7 zu erleichtern, liefert Scala 2.8 ein Package Object (dazu mehr in der nächsten Folge) mit Aliasen, sodass bestehender Code z.B. weiterhin scala.List nutzen kann. Für neuen Code sei jedoch empfohlen, die neue Package-Struktur zu beherzigen.
Weiterhin wurden etliche “Altlasten” nicht sofort entfernt, sonder als @deprecated gekennzeichnet. Z.B. sollen wir nun nicht mehr Iterable.elements verwenden, sondern Iterable.iterator. Gemäß der Deprecation-Policy von Scala bleiben diese “veralteten” Typen und Methoden mindestens in allen 2.8.x-Versionen erhalten.
Für die Konvertierung von Java-Collections in Scala-Collections und umgekehrt gab es bisher das Package scala.collection.jcl. Dieses existiert in Scala 2.8 nicht mehr, an dessen Stelle tritt das Objekt scala.collection.JavaConversions, das eine Reihe von Implicit Conversions definiert. Schick hierbei, dass diese Konvertierungen bidirektional und reversibel sind. Allerdings wurden - offenbar bewusst - nur wenige und überwiegend high-level Collection-Typen berücksichtigt, z.B. Iterable und Iterator.
Fazit
Solange wir "nur" die existierenden Collection-Typen nutzen, ändert sich nicht viel. Zumal wir durch die Umstiegs-Hilfe weiterhin die alten Packages verwenden dürfen. Für Power-User, die eigene Collection-Typen schreiben möchten, steht jedoch eine hervorragende Basis bereit. Soviel zu den "neuen" Collections. Die nächste Folge wird sich wie erwähnt mit Package Objects befassen.




