Archive for Juli 2011|Monthly archive page

Teil 9: Klassen und Objekte

Die bisher angesprochenen Themen waren zwar alle ganz nett, erlauben uns aber noch nicht viel mehr als nur mit der REPL zu arbeiten. Wir könnten vielleicht schon ein paar kleine Skripte schreiben, aber das ist nicht Scalas Stärke. Gehen wir also ein wenig weiter zum Arbeiten mit Objekten.

Klassen

Das Erste was wir zum Erstellen von Objekten benötigen ist eine Klasse. Wie aus vielen anderen Programmiersprachen schon bekannt, benötigen wir dafür das class-Schlüsselwort:

class Person

So fertig, wir haben unser ersten Objekt. Nur können wir damit leider noch nicht viel anfangen. Fügen wir dem Objekt also ein paar Attribute hinzu:

class Person(name: String, age: Int)

Das sieht doch mal interessant aus. Und es ist sehr kompakt. Was wir hier gemacht haben ist nichts anderes, als dass wir der Klasse Person einen Konstruktor verpasst haben, der als Parameter einen String und einen Int erwartet. Der Konstruktor einer Klasse wird in Scala immer mit runden Klammern direkt hinter dem Klassennamen notiert. Falls wir keine runde Klammern erstellen kreiert der Scala-Compiler für uns einen Default-Konstruktor, der keine Parameter erwartet.

Scalas „way of life“ lautet dabei immer: Erzeuge so wenig Overhead wie möglich. Wir dürfen also sowohl Runde als auch geschweifte Klammern, die den Klassenrumpf aufnehmen, weglassen wenn sie keinen Inhalt haben.

Wir wollen jetzt aber endlich ein Objekt haben mit dem wir etwas anfangen können. Dafür müssen wir nicht mal viel ändern:

class Person(val name: String, val age: Int)

Das val vor dem Attributnamen weist den Scala-Compiler an, unser Attribut zu einem Feld der Klasse zu machen, auf das man von außerhalb zugreifen kann:

scala> class Person(val name: String, val age: Int)
defined class Person

scala> val p = new Person("Franz", 35)
p: Person = Person@2bec5408

scala> p.name
res37: String = Franz

scala> p.age
res38: Int = 35

scala> p.name = "Hugo"
:9: error: reassignment to val
       p.name = "Hugo"
              ^

Was wir hier erzeugt haben ist ein unveränderliches Objekt. Wir können es einmal erstellen, aber nie mehr ändern. Wollen wir dagegen die Möglichkeit haben die Felder eines Objekts nach der Instanziierung noch zu ändern müssen wir lediglich das val gegen ein var eintauschen:

scala> class Person(var name: String, var age: Int)
defined class Person

scala> val p = new Person("Franz", 35)
p: Person = Person@29a430a0

scala> p.name = "Hugo"
p.name: String = Hugo

In diesem Fall wäre aber davon abzuraten die Variable veränderbar zu halten, da wir sonst mit jeder Änderung eines Feldes auch das jeweils andere Feld ändern müssten (andere Personen haben ja wohl nicht das gleiche Alter).
Was hier unter der Haube vorgeht ist eigentlich ganz einfach. Das was wir in anderen Programmiersprachen von Hand erledigen müssen erledigt für uns der Scala-Compiler. Er erzeugt für die Klasse ein Feld und je nach dem ob wir das Feld veränderlich halten wollen generiert er uns noch einen Getter und einen Setter. In Scala wird den Gettern und Settern üblicherweise kein get und kein set vorangestellt (anders als bei Java, bei dem dies die übliche Vorgehensweise wäre). Beim Getter wäre es damit zu begründen, dass in Scala sowieso alles einen Wert zurück gibt – Scala Unit funktioniert anders wie Javas void. Beim Setter dagegen wissen wir anhand der runden Klammern ja, dass eine Operation Seiteneffekte produzieren kann. Der Getter trägt deshalb auch keine Klammern und kann auch nicht mit Klammern aufgerufen werden:

scala> p.name()
:10: error: not enough arguments for method apply: (index: Int)Char in class StringOps.
Unspecified value parameter index.
              p.name()
                    ^

Was die Fehlermeldung in Verbindung mit der Methode apply genau zu bedeuten hat werde ich bald erklären. Es wird Zeit, dass wir anfangen unserer Klasse eigene Methoden zu geben, damit wir ein wenig mehr damit machen können als nur ihre Felder anzuschauen. Ich möchte von dem Personen-Beispiel weg und hin zu einem Beispiel aus der Mathematik gehen: Wir wollen mit Scala rationale Zahlen verarbeiten können, was von Haus aus nicht geht. Also erweitern wir die Sprache eben kurzerhand damit.

Rational Zahlen besitzen einen Nenner und einen Zähler:

class Rational(numerator: Int, denominator: Int)

Wenn wir ein Objekt dieser Klasse erzeugen, dann haben wir leider das Problem, dass die REPL uns nur unverständliches Zeug ausgibt:

scala> new Rational(3, 5)
res42: Rational = Rational@1b1bcdee

Was wir hier sehen ist der Klassenname gefolgt von einem @-Zeichen und der hexadezimalen Schreibweise des HashCodes des Objekts.

Für alle die mit Java bzw. mit der JVM noch nichts zu tun hatten: Der HashCode gibt die Attribute eines Objektes umgewandelt zu einem Int zurück. Wie genau diese Umwandlung vonstatten geht soll jetzt irrelevant sein. Wichtig ist mehr, dass der HashCode für jedes Objekt möglichst einzigartig ist und sich nicht ändert wenn sich das Objekt auch nicht ändert.

Wir können die Ausgabe ändern indem wir die Methode toString überschreiben. Sie wird von der Methode, die für die Konsolenausgabe verantwortlich ist aufgerufen und legt also fest mit was sich unser Objekt auf der Konsole schmücken darf.

class Rational(numerator: Int, denominator: Int) {
  override def toString = numerator+"/"+denominator
}

scala> new Rational(3, 5)
res44: Rational = 3/5

Die Ausgabe sieht doch gleich viel besser aus. Das override gibt an, dass unsere Methode, eine gleichnamige Methode aus dem Vaterobjekt überschreibt. Aber was ist unser Vaterobjekt? Das ist Object, eine Klasse der JVM, die oberste Klasse überhaupt. Jedes Objekt in Scala – auch Any – ist vom Typ Object. In ihm sind Methoden wie toString und hashCode definiert.

Auf das nächste, auf das wir achten wollen, ist, ob unser Konstruktor auch mit korrekten Daten initialisiert wird. Was würde z.B. passieren wenn wir im Nenner eine null angeben würden? Den Bruch `3/0` gibt es ja überhaupt nicht. Um dies zu unterbinden stellt uns Scala die require-Methoden bereit:

class Rational(numerator: Int, denominator: Int) {
  require(denominator != 0)

  override def toString = numerator+"/"+denominator
}

scala> new Rational(3, 0)
java.lang.IllegalArgumentException: requirement failed

In Scala gehört jeglicher Code innerhalb des Klassenrumpfes zum Konstruktor und wird deshalb ausgeführt sobald der Konstruktor aufgerufen wird. Da dies immer dann der Fall ist wenn ein neues Objekt instanziiert wird kann unser `require` auch sofort überprüfen ob die Attribute korrekt gesetzt wurden.

Als nächstes wollen wir schauen, dass wir nur gekürzte Brüche aufnehmen. Ein Bruch wie 5/9 ist doch viel einfacher zu lesen wie 1265/2277, oder? Kürzen können wir unseren Bruch mit dem größten gemeinsamen Teiler (ggT) der beiden Zahlen:

class Rational(numerator: Int, denominator: Int) {
  require(denominator != 0)

  val g = gcd(numerator, denominator)
  val n = numerator / g
  val d = denominator / g

  override def toString = n+"/"+d

  def gcd(a: Int, b: Int): Int = {
    import scala.math.abs
    def loop(a: Int, b: Int): Int = if (b == 0) a else loop(b, a%b)
    loop(abs(a), abs(b))
  }
}

Bevor ich erkläre was hier alles neu dazugekommen ist wollen wir uns erst anschauen ob der Code das macht was er tun soll:

scala> new Rational(1265, 2277)
res50: Rational = 5/9

Da, er macht es, das ist schon mal toll. Nun zu den ganzen Erklärungen. Die Methode gcd prüft ob ein ggT existiert und gibt diesen zurück. Danach kürzen wir den Nenner und Zähler aus dem Konstruktor. Da die beiden Attribute ohne ein val oder var deklariert wurden, stehen sie außerhalb der Klasse nicht mehr zur Verfügung. Wir müssen uns also nicht mehr weiter darum kümmern. Die drei neuen Felder g, n und d dagegen wurden mit val deklariert und können nun von außerhalb der Klasse frei eingesehen werden. Das ist aber nicht weiter schlimm, da sie unveränderlich sind.

Anmerkung: Der Scala-Compiler erzeugt auch bei Feldern, die außerhalb des Konstruktors erzeugt wurden Getter und Setter. Wenn wir also den Feldnamen aufrufen greifen wir nicht direkt auf das Attribut sondern auf dessen Getter zu.

Geändert wurde noch die Methode toString. Sie gibt nun die neuen Werte zurück und nicht die, die im Konstruktor angegeben wurden. In der Methode gcd finden wir etwas, das wir so noch nicht gesehen haben: ein import-Statement in einer Methode. In Scala ist es prinzipiell möglich überall ein import-Statement anzugeben. Wenn wir es in der Methode machen, dann gilt der import auch nur für die Methode und nicht für außerhalb. Die Tailrekursive-Methode, die den ggT berechnet sollten wir bereits kennen.

Wir haben unserer Klasse jetzt unsere erste eigene Methode hinzugefügt, machen wir doch gleich weiter. Der Übersichtlichkeit wegen werde ich ab jetzt nur noch die Änderungen und Neuerungen an der Klasse Rational niederschreiben.

// in Rational
def add(r: Rational) = new Rational(n*r.d + r.n*d, d*r.d)

scala> val r1 = new Rational(2, 9)
r1: Rational = 2/9

scala> val r2 = new Rational(4, 9)
r2: Rational = 4/9

scala> r1.add(r2)
res51: Rational = 2/3

Die Methode add erzeugt einen neuen Bruch, anstatt die Felder des bereits bestehenden zu ändern. Das würde auch gar nicht gehen, da sie ja mit val deklariert wurden. Besonders toll anzusehen ist, dass unser neuer Bruch auch gleich gekürzt wird. Aber Moment einmal. Können wir in Scala denn nicht jedes Zeichen als Methodenname benutzen? Wieso verwenden wir nicht +? Würde doch viel besser aussehen.

// in Rational
def + (r: Rational) = new Rational(n*r.d + r.n*d, d*r.d)

scala> r1 + r2
res52: Rational = 2/3

Stimmt, sieht echt gut aus.

Ob man „Operatorüberladung“ jetzt gut findet oder nicht muss man selbst entscheiden. Ich finde es ein mächtiges Werkzeug, das in den richtigen Händen sehr zum Codeverständnis beitragen kann und werde es deshalb auch ausgiebig nutzen, wenn es angebracht ist. Falls das jemand anders sieht steht es ihm frei den Methoden „normale“ Namen zu geben.
Weiterhin möchte ich noch anmerken, dass man in Scala nicht von Operatorüberladung spricht, da es keine wirklichen Operatoren gibt. Jedes Zeichen (bis auf die von der Sprache reservierten) kann in einem Identifier genutzt werden, weshalb keine Unterscheidung zwischen den „normalen“ und den Sonderzeichen gibt.

Das können wir für die anderen Operationen wiederholen:

// in Rational
def - (r: Rational) = new Rational(n*r.d - r.n*d, d*r.d)
def * (r: Rational) = new Rational(n*r.n, d*r.d)
def / (r: Rational) = new Rational(n/r.n, d/r.d)

scala> r2 - r1
res53: Rational = 2/9

scala> r1 * r2
res54: Rational = 8/81

scala> r2 / r1
res55: Rational = 2/1

Wir haben es geschafft. Wir haben unsere erste eigene Klasse erstellt, mit der wir sogar etwas anfangen können. Für den Anfang ist das schon mal gar nicht schlecht. Aber es gibt noch so viel was wir alles in unsere Klasse einbauen können.

In Scala gibt es übrigens eine Operatorpriorität, obwohl es keine Operatoren im klassischen Sinne gibt. Bei Eingabe folgender Gleichung erhalten wir auch das korrekte Ergebnis:

scala> 4+3*2
res20: Int = 10

Scala parst den Ausdruck als

scala> 4+(3*2)
res21: Int = 10

Die Priorität einer Methode hängt vom ersten Zeichen der Methode ab. Wie die genaue Reihenfolge ist könnt ihr hier nachschlagen.

Hilfskonstruktoren

Was z.B. noch störend ist, ist das wir unsere Klasse immer mit einen Zähler und auch einem Nenner initialisieren müssen. Eine rationale Zahl muss aber nicht unbedingt ein Bruch sein, zumindest müssen wir sie nicht so schreiben. 123 ist leichter verständlich als 123/1. Um unsere Klasse mit nur einem Zähler zu initialisieren benötigen wir einen zweiten Konstruktor. In Scala spricht man bei mehrfach vorhandenen Konstruktoren von Hilfskonstruktoren (engl.: auxiliary constructor), da sie an sich nichts anderes machen als den Standardkonstruktor aufzurufen. Erzeugen können wir einen Hilfskonstruktor mit def this(…):

class Rational(numerator: Int, denominator: Int) {
  ...
  def this(numerator: Int) = this(numerator, 1)
  ...
}

scala> val r1 = new Rational(5)
r1: Rational = 5/1

Die erste Anweisung, die in einem Hilfskonstruktor getätigt werden muss ist der Aufruf eines anderes Konstruktors. Das kann der Standardkonstruktor aber auch ein anderen Hilfskonstruktor sein. Innerhalb dieser Konstruktoren haben wir also keine Möglichkeit die Parameter vor Objekterzeugung zu überprüfen. Dies sollte innerhalb des Klassenrumpfes oder im companion object erledigt werden und sonst nirgends. Wir werden später noch auf das companion object zurückkommen.
Neben einem Hilfskonstruktor besteht auch die Möglichkeit einfach ein benanntes Argument einzuführen:

class Rational(numerator: Int, denominator: Int = 1) { ... }

scala> val r1 = new Rational(5)
r1: Rational = 5/1

Das macht den Code ein wenig kürzer und übersichtlicher. Da wir mit unserem Hilfskonstruktor sowieso nicht viel anfangen können, empfehle ich, benannte Argumente vorzuziehen.

object

Wenn wir auf das Kapitel über Collections zurückbilcken fällt uns auf, dass wir unsere Objekte stets ohne das new-Schlüsselwort instanziiert haben. Tatsächlich können wir viele Collections gar nicht mit new instanziieren:

scala> new Seq(1, 2, 3)
:8: error: trait Seq is abstract; cannot be instantiated
              new Seq(1, 2, 3)
              ^

Wie kommt das? Was hier vor sich geht ist der Aufruf des companion objects immer dann wenn wir das new zur Objektinstanziierung auslassen.

Das companion object stellt eine Möglichkeit dar auf den Kontext einer Klasse zuzugreifen ohne von ihr ein Objekt instanziieren zu müssen. Wie das aussehen könnte sehen wir hier:

object Rational {
  def apply(numerator: Int, denominator: Int = 1) = new Rational(numerator, denominator)
}

Wenn wir den Code in der REPL ausführen, bekommen wir erst mal eine Warnung:

scala> object Rational {
     |   def apply(numerator: Int, denominator: Int = 1) = new Rational(numerator, denominator)
     | }
defined module Rational
warning: previously defined class Rational is not a companion to object Rational.
Companions must be defined together; you may wish to use :paste mode for this.

Ein object ist erst dann das companion object einer Klasse wenn es den gleichen Namen trägt und wenn es in der gleichen Datei definiert wurde wie die Klasse. Den gleichen Namen hat es, aber wir haben keine Datei in der sich die Klasse Rational befindet. Diese haben wir erst wenn wir unseren Code später mit scalac übersetzen lassen wollen. Also machen wir das was uns die REPL vorschlägt. Den Code im paste-Modus einfügen. Haben wir das getan sollten wir folgenden Code ausführen können:

scala> Rational(5)
res0: Rational = 5/1

Kommen wir also nun zu den Erklärungen was da alles vor sich geht. Das object-Schlüsselwort unterscheidet sich insofern vom class-Schlüsselwort als dass wir dort alle definierten Inhalte ohne Objektinstanziierung aufrufen dürfen:

object Test {
  def printHello() {
    println("hello")
  }
}

scala> Test.printHello
hello

Das erklärt also schon einmal weshalb wir das new bei Rational nicht mehr benötigen. Es erklärt aber noch nicht wie wir trotzdem ein Rational-Objekt erstellen können. Wenn wir uns die apply-Methode anschauen sehen wir, dass sie diese Objektinstanziierung für uns erledigt:

scala> Rational.apply(5)
res13: Rational = 5/1

Aber wieso können wir die Methode auch einfach auslassen? Die Antwort darauf lautet: Compiler-Magic. Die apply-Methode kann aufgerufen werden ohne dass man ihren Namen aufrufen muss. Der Compiler prüft ob eine apply-Methode zum Namensraum von class oder object gehört und ruft diese automatisch auf wenn nach dem Namen runde Klammern folgen. Ein Beispiel:

Wir haben:

AnyClass(1, 2, 3)
AnyClass()

Der Compiler macht daraus:

AnyClass.apply(1, 2, 3)
AnyClass.apply()

Jetzt wissen wir auch was passiert wenn wir eine Collection ohne new erzeugen:

scala> List(1, 2, 3)
res16: List[Int] = List(1, 2, 3)

scala> List.apply(1, 2, 3)
res17: List[Int] = List(1, 2, 3)

Das gleiche passiert übrigens wenn wir versuchen auf ein Element einer Collection zuzugreifen:

scala> res16(0)
res18: Int = 1

scala> res16.apply(0)
res19: Int = 1

Der einzige Unterschied ist, dass verschiedene apply-Methoden aufgerufen werden. Beim ersten Mal wird die apply-Methode des companion objects von List aufgerufen, beim zweiten Mal ist die apply-Methode der Klasse List dran. Neben der apply-Methode gibt es noch eine update- und eine unapply-Methode, die ebenfalls nicht explizit aufgerufen werden müssen. Der Compiler stellt uns für alle drei Methoden syntaktischen Zucker zur Verfügung. Was die beiden letztgenannten machen kann uns momentan egal sein, da wir sie noch nicht benötigen. Ich werde später darauf eingehen.

Falls ein object zu einem compnion object einer Klasse wird, wird die Klasse zur companion class des objects. Die beiden gehen eine Art Bindung ein, die es dem companion object erlaubt auf die nicht öffentlichen Felder der companion class zugreifen zu dürfen.

Zum Schluss noch den kompletten Code von Rational. Einzige Änderung ist, dass ich innerhalb der Klasse alle new entfernt habe:

object Rational {
  def apply(numerator: Int, denominator: Int = 1) = new Rational(numerator, denominator)
}

class Rational(numerator: Int, denominator: Int = 1) {
  require(denominator != 0)

  val g = gcd(numerator, denominator)
  val n = numerator / g
  val d = denominator / g

  def + (r: Rational) = Rational(n*r.d + r.n*d, d*r.d)
  def - (r: Rational) = Rational(n*r.d - r.n*d, d*r.d)
  def * (r: Rational) = Rational(n*r.n, d*r.d)
  def / (r: Rational) = Rational(n/r.n, d/r.d)

  override def toString = n+"/"+d

  def gcd(a: Int, b: Int): Int = {
    import scala.math.abs
    def loop(a: Int, b: Int): Int = if (b == 0) a else loop(b, a%b)
    loop(abs(a), abs(b))
  }
}
Advertisements

Operatorpriorität

Die Priorität eines Operators hängt vom ersten Zeichen der Methode ab. Der Ausdruck 3+4*2 wird also nicht als (3+4)*2 geparst, sondern mathematisch korrekt als 3+(4*2).

Die genaue Reihenfolge kann in der Scala Reference (Section 6.12.3 Infix Operations) nachgeschlagen werden:

(all letters)
|
^
&
< >
= !
:
+ -
* / %
(all other special characters)

Aufsteigende Reihenfolge, erstes Element besitzt niedrigste Priorität.

Es ist wichtig, dass ihr darauf achtet nicht durch eventuelle „Methodenvertauschungen“ Probleme zu bekommen.

Der Ausdruck 3 add 4 mul 2 wird anders ausgewertet als 3+4*2.

Schlüsselwörter in Scala

Hier eine Auflistung aller Schlüsselwörter und -symbole, die nicht regulär als eigene Identifier verwendet werden dürfen:

Schlüsselwörter (39)

abstract case catch class def do else extends false final finally for forSome if implicit import lazy match new null object override package private protected return sealed super this throw trait try true type val var while with yield

Symbole (12)

_ : = => <- <: <% >: # @

⇒ (Unicode \u21D2) equivalent zu: =>
← (Unicode \u2190) equivalent zu: <-

Teil 8: Pattern Matching

Wir haben schon viel von Scala kennen gelernt, das bisherige Wissen reicht aber noch nicht um in Scala größere Programme schreiben zu können. Bevor wir endlich zur Objektorientierung kommen möchte ich euch noch das Thema Pattern Matching näher bringen.

In der einfachsten Form kann man Pattern Matching mit Mehrfachverzweigungen aus Java vergleichen. Sie sehen ähnlich aus und verhalten sich auch ähnlich, sind aber ungemein komfortabler.

int i = ...;
switch (i) {
  case 2:
    break;
  case 3: case 4: case 7:
    break;
  case 12:
    break;
  default:
    break;
}

Javas switch-case erlaubt lediglich das testen auf Zahlen, Enums und Strings (Java7). Das ist aber oft nicht ausreichend. Was wenn ich ein Objekt auf seinen Typ überprüfen möchte? Dann muss ich auf längere und vor allem hässliche if-else-instanceof-Kaskaden zurückgreifen. Auch Mehrfachvergleiche sind nicht besonders intuitiv. Man muss sich den fall-through zu Nutze machen und jede Überprüfung von neuem mit einem `case` einleiten. Der fall-through ist sowieso total unsinnig – habt ihr den jemals gebraucht? Also ich nicht und ich wäre glücklicher wenn man sich das `break` sparen könnte.

In Scala hat man sich dessen angenommen und die alte Mehrfachverzweigung durch das viel mächtigere Pattern Matching ersetzt:

val i = ...
i match {
  case 2 =>
  case 3 | 4 | 7 =>
  case 12 =>
  case _ =>
}

Das Schlüsselwort `switch` wurde durch `match` ersetzt und wird hinter die zu überprüfende Variable gesetzt. Mit case-Statements folgen dann die Überprüfungen. Die Überprüfung ist alles was sich zwischen dem case und dem `=>`-Symbol befindet. Das Symbol, das aussieht wie ein Pfeil hat dabei die gleiche Bedeutung wie der Punkt in Javas switch-case. So weit so gut, das war es aber auch schon mit den Gemeinsamkeiten.

Pattern Matching unterstützt kein fall-through. Die Ausführung einer Verzweigung endet mit Beginn einer Neuen – es wird also auch kein spezielles Schlüsselwort benötigt um das Ende zu kennzeichnen. Anhand des zweiten case-Statement ist zu erkennen, dass mehrere Überprüfungen komfortabel mit einem Statement gemacht werden können. Die zu überprüfenden Werte werden einfach mit einem senkrechten Strich voneinander getrennt – dem branchenüblichen Oder-Zeichen. Ein weiteres Merkmal ist, dass jedes überprüfte Element auf eine Überprüfung passen muss, ist dies nicht der Fall wird ein MatchError geworfen. Wenn also die Möglichkeit besteht, dass die Überprüfungen eines match-Statements nicht alle Werte abdecken können, dann muss ein default-Fall eingefügt werden. Diesen erstellt man in dem man auf den Unterstrich matcht. Wir erinnern uns: Der Unterstrich steht für irgendwas, er kann alles sein. Er wird immer dann gematcht, wenn kein nichts anderes passt. Das Pattern Matching wird beendet sobald korrekt gematcht wurde. Nach der Abarbeitung eines erfolgreichen case werden die restlichen case also nicht mehr ausgeführt. Dies bedeutet, dass der default-Fall immer als letztes geprüft werden muss, alles was danach kommt ist nämlich unerreichbar. War doch gar nicht so schwer, oder? Wir müssen weniger Code schreiben um das gleiche zu erreichen wie Javas switch-case.

Aber das war erst der Anfang.

Pattern Matching kann auf beliebige Typen prüfen – schauen wir uns dazu gleich das nächste Beispiel an:

val any: Any = ...
val matched = any match {
  case 2 | 4 | 7 => "a number"
  case "hello" => "a string"
  case true | false => "a boolean"
  case 45.35 => "a double"
  case _ => "an unknown value"
}

Wir haben irgendein Any und wollen wissen was genau es ist – kein Problem. Dadurch, dass in Scala alles ein Wert zurück gibt haben wir die Möglichkeit die letzte Anweisung innerhalb eines case-Blocks an eine Variable zu binden. Bitte beachtet, dass wir keine geschweiften Klammern benötigen um innerhalb eines match einen Block zu kennzeichnen – anhand des `=>` und des case kann der Compiler zuverlässig das Vorhandensein eines solchen Blocks erkennen:

val matched = any match {
  case 2 | 4 | 7 =>
    doSomething()
    "a number"
  case "hello" =>
    doSomething()
    "a string"
  case true | false =>
    doSomething()
    "a boolean"
  case 45.35 =>
    doSomething()
    "a double"
  case _ =>
    doSomething()
    "an unknown value"
}

Durch die Typinferenz erkennt der Compiler, dass die Variable `matched` vom Type String ist – ich hoffe zwar, dass ihr euch das mittlerweile selbst zusammenreimen könnt, aber man kann es ja nie oft genug erwähnen.

Das war bisher ja alles ganz nett, aber besonders vom Stuhl haut es uns nicht gerade. Das kann aber noch kommen, denn match kann noch mehr. Was ist wenn wir z.B. innerhalb eines gematchten Statements auf das zugreifen wollen was gematcht wurde? Anstatt also bei einer gematchten Zahl nur zurück zu geben, dass wir eine Zahl haben wäre es doch schön auch den Wert dieser Zahl zurückzugeben. Wir könnten jetzt umständlich das zu matchende Objekt casten um dann auf dessen Methoden zugreifen zu können, aber warum sollten wir das tun wenn Scala das schon für uns erledigt?

val matched = any match {
  case n: Int => "a number with value: "+n
  case _: String => "a string"
  case true | false => "a boolean"
  case d @ 45.35 => "a double with value "+d
  case d => "an unknown value "+d
}

Wir haben die Möglichkeit anstatt auf die Instanz eines bestimmten Typs auch einfach nur auf den Typ zu überprüfen. Dazu müssen wir bloß einen einen Identifier hinter das case schreiben und den Typ mit einem Doppelpunkt an den Identifier binden, wie im ersten case-Statement der Fall. Wir könnten das so lesen: Prüfe ob das gematchte Objekt vom Typ Int ist und wenn ja, dann binde das Objekt an eine Variable namens `n` und weise ihr den Typ Int zu. Es genügt nur den Namen der Variable anzugeben, wie bei den Generatoren und Definitionen der for-Expression benötigen wir kein `val` oder `var`. Die Sichtbarkeit dieser Variable beschränkt sich auf das case-Statement wir können also bei mehreren Überprüfungen auch mehrmals den gleichen Variablennamen nehmen – wie zu sehen bei den letzten beiden Statements.

Sollten wir nur auf eine beliebige Instanz eines Typs prüfen wollen, so besteht die Möglichkeit den Variablennamen wegzulassen und statt dessen einen Unterstich zu nehmen wie beim zweiten case-Statement gezeigt.

Eine weitere Besonderheit stellt das `@`-Zeichen dar. Es bedeutet so viel wie: Prüfe ob ein Objekt gleich der Instanz eines Typs ist und wenn ja, dann binde diese Instanz an einen Identifier. Im obigen Beispiel prüfen wir ob ein Double mit dem Wert 45.35 vorliegt. Sollte das der Fall sein wird dieser Wert an die Variable `d` gebunden, die wir dann benutzen können.

Um den Unterschied zwischen `@` und `:` nochmal klar und deutlich hervorzuheben: Ersteres prüft auf eine Instanz, letzteres auf einen Typ.

Neben einem Unterstich haben wir die Möglichkeit im letzten Statement auch einfach einen Identifier zu benutzen. Das können wir immer dann machen wenn wir noch auf das gematchte Objekt zugreifen wollen.

Schauen wir uns das ganze also mal in Aktion an:

def matching(any: Any) = any match {
  case n: Int => "a number with value: "+n
  case _: String => "a string"
  case true | false => "a boolean"
  case d @ 45.35 => "a double with value 45.35"
  case d => "an unknown value "+d
}

scala> matching(734)
res0: java.lang.String = a number with value: 734

scala> matching("hello")
res1: java.lang.String = a string

scala> matching(true)
res2: java.lang.String = a boolean

scala> matching(45.35)
res3: java.lang.String = a double with value 45.35

scala> matching(Nil)
res4: java.lang.String = an unknown value List()

Funktioniert alles. Wenn wir die bisher kennen gelernte Möglichkeiten miteinander kombinieren, können wir richtig tolle Objekte überprüfen:

def matching(xs: List[Int]) = xs match {
  case 5 :: 3 :: Nil => "List contains 5 and 3"
  case _ :: 7 :: _ => "Second element of List is 7"
  case List(1, tail @ _*) => "first element is 1 and tail is: "+tail
  case Nil => "Nil"
}

Im ersten Fall prüfen wir ob die Liste zwei Elemente besitzt (Nil wird nicht als Element angesehen, es ist einfach nur das Ende), im zweiten Fall ob das zweite Element 7 ist. Mit dem `::`-Symbol können wir die Liste einfach zusammen bauen, das ist uns bereits bekannt.

Pattern Matching erlaubt uns Objekte beliebiger Komplexitätsstufe zusammenzubauen indem wir einfach den Konstruktor mit den Elementen, die uns interessieren, aufrufen. So geschehen im dritten Fall. Das erste Element dort soll 1 sein, der Rest wird an die Variable `tail` gebunden. Die dort auftauchende Symbole sollten uns nicht unbekannt sein. Wir haben eine so ähnliche Schreibweise schon mal in Verbindung mit varargs gesehen. Hier heißt es so viel wie: Binde beliebig viele Objekte an die Variable `tail`.

Diese Schreibweise leuchtet euch nicht ein? Gut, mir auch nicht. 😉
Einfacher wäre es `List(1, tail*)` zu schreiben, dies funktioniert aber aufgrund eines Bugs im Compiler nicht. Ein Bug-Report dazu ist schon vorhanden, jetzt muss er nur noch umgesetzt werden. Bis es soweit ist müssen wir uns mit der etwas umständlicheren Variante abfinden.

Guards

Eine weitere Eigenschaft des Pattern Matching sind die sogenannten Guards. Sie funktionieren ähnlich wie `filter` aus den for-Expressions:

def matching(any: Any) = any match {
  case n: Int  if n > 10 && n < 100 => "int"
  case d: Double if scala.math.round(d) == 20 => "double"
  case s: String => if (s.size != 6) "string" else s
  case _ => "unknown"
}

Mit Hilfe der Guards können wir überprüfen die Überprüfungen noch weiter einschränken. Bitte beachtet, dass es einen Unterschied macht ob wir einen Guard (linke Seite von `=>`) oder eine if-Expression (rechte Seite von `=>`) verwenden. Die rechte Seite wird erst ausgeführt wenn das Muster auf der linken Seite passt. Das bedeutet, dass die rechte Seite auf jeden Fall einen else-Zweig besitzen muss, auf der linken Seite dagegen wird bei einem else-Fall einfach zum nächsten Muster weiter gesprungen:

scala> matching(56)
res20: java.lang.String = int

scala> matching(128)
res21: java.lang.String = unknown

scala> matching(20.345)
res22: java.lang.String = double

scala> matching("myname")
res23: java.lang.String = myname

scala> matching("hello")
res24: java.lang.String = string

Ich bin groß und du bist klein

In Verbindung mit Pattern Matching gibt es doch tatsächlich etwas wichtiges zu beachten: Es ist nicht egal ob wir einen gematchten Wert groß oder klein schreiben. Das Einhalten der Naming-Conventions (Variablen „lowerCamelCase“, Objekte „UpperCamelCase“) ist dient also nicht nur dem Verständnis des Codes, es dient auch zum Vorbeugen von Fehlern.

Schauen wir uns das mal genauer an:

scala> 5 match { case a: Int => "int" case _ => "unknown" }
res26: java.lang.String = int

scala> 5 match { case A: Int => "int" case _ => "unknown" }
<console>:1: error: '=>' expected but ':' found.
       5 match { case A: Int => "int" case _ => "unknown" }
                       ^

Da der Compiler den Anfang und das Ende eines Blocks bei den case-Statements selbst erkennen kann müssen wir die einzelnen case-Statements nicht mit einem Strichpunkt voneinander trennen.

Beim Code mit einem großgeschriebenen Variablennamen beschwert sich der Compier. Der Grund ist, dass der Compiler durch den großen Anfangsbuchstaben nun keine Variable mehr erwartet, sondern einen Extraktor! Er versucht den Extraktor der Klasse A aufzurufen. Da A ja aber schon ein Typ ist dürfen wir keinen weiteren Typ mehr angeben (was wir durch das `: Int` aber tun). So kommt es zur Fehlermeldung. Es ist also unerlässlich, dass Variablen immer klein geschrieben werden.

Auf ein weiteres Problem mit den Naming-Conventions stoßen wir wenn wir versuchen auf den Inhalt einer bereits existierenden Variable zu matchen:

scala> val a = 5
a: Int = 5

scala> 7 match { case a => "5" case _ => "unknown" }
<console>:8: error: unreachable code
              7 match { case a => "5" case _ => "unknown" }
                                                ^

Der Compiler beschwert sich wegen unerreichbarem Code, warum? Wir glauben, dass wir den Inhalt der Variable `a` prüfen, das stimmt aber nicht. Das Pattern Matching Konstrukt erzeugt eine neue Variable a und bindet an diese jeden Wert, den wir eingeben. Dadurch kann das zweite Muster nie erreicht werden, da schon das erste true ergibt. Wollen wir auf den Wert einer Variable prüfen müssen wir diese in Backticks schreiben:

scala> 7 match { case `a` => "5" case _ => "unknown" }
res32: java.lang.String = unknown

scala> 5 match { case `a` => "5" case _ => "unknown" }
res33: java.lang.String = 5

Damit teilen wir dem Compiler mit, dass er keine neuen Variable mehr erzeugen, sondern stattdessen eine schon vorhandene dereferenzieren soll.

Backticks kommen auch zum Einsatz wenn wir z.B. eine Methode aus einer Java-Lib aufrufen wollen, die gleich heißt wie ein Schlüsselwort in Scala. So können wir nicht

obj.match(param)

schreiben, da der Compiler die Methode match nicht als Methode sondern als Schlüsselwort erkennen würde. Die Schreibweise

obj.`match`(param)

funktioniert dagegen.

Pattern Matching everywhere

Eine der größten stärken von Pattern Matching ist sicherlich, dass wir es überall anwenden können, nicht nur innerhalb eines match-Blocks:

scala> val head :: tail = List(1, 2, 3)
head: Int = 1
tail: List[Int] = List(2, 3)

Was wir hier machen ist eigentlich ganz einfach. Wir haben eine Liste und binden dessen Elemente an verschiedene Variablen. Das macht besonders viel Sinn, wenn wir eine Methode haben, die komplexe Objekte zurück gibt und wir deren Werte an eine Variable binden wollen.

Um nochmal das Beispiel mit den Tuplen aus einem vorherigen Artikel aufzugreifen:

def heavyCalculation() = {
  val memoryUsage = 50
  val cpuUsage = 91
  val networkUsage = 31
  (memoryUsage, cpuUsage, networkUsage)
}

Die naheliegendste Version unseres Ziels würden wir jetzt ungefähr so erreichen:

scala> val usage = heavyCalculation()
usage: (Int, Int, Int) = (50,91,31)

scala> val memoryUsage = usage._1
memoryUsage: Int = 50

scala> val cpuUsage = usage._2
cpuUsage: Int = 91

scala> val networkUsage = usage._3
networkUsage: Int = 31

Aber warum nicht einfach folgendes schreiben?

scala> val (memoryUsage, cpuUsage, networkUsage) = heavyCalculation()
memoryUsage: Int = 50
cpuUsage: Int = 91
networkUsage: Int = 31

Was ist wohl einfacher und übersichtlicher? Wir erzeugen einfach einen Tuple3 und prüfen mit Pattern Matching ob wir der Rückgabewert der Methode an eben diesen Tuple3 binden können. Ist dies der Fall werden die Werte dann an die Variablen gebunden und wir können schön unkompliziert weiter programmieren.

Das können wir uns auch innerhalb von for-Expressions zu nutze machen:

scala> val m = Map(1 -> "a", 2 -> "b", 3 -> "c")
m: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> a, 2 -> b, 3 -> c)

scala> for ((pos, letter) <- m) println("the letter '"+letter+"' has the "+pos+". position in the alphabet")
the letter 'a' has the 1. position in the alphabet
the letter 'b' has the 2. position in the alphabet
the letter 'c' has the 3. position in the alphabet

Da die Map uns über lauter Tuple2 iterieren lässt können wir diese auch einfach auspacken und dann innerhalb der for-Expression damit arbeiten.

Das Prinzip funktioniert auch mit anderen Objekten gleich wie beim Tuple:

scala> val seq = Seq(Seq(1, 2, 3), Seq(4, 5, 6), Seq(7, 8, 9))
seq: Seq[Seq[Int]] = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))

scala> val Seq(Seq(_*), middle @ Seq(4, 5, 6), Seq(_*)) = seq
middle: Seq[Int] = List(4, 5, 6)

Wir können die Seq so weit auseinanderbauen wie wir wollen und uns die Daten herausgreifen, die wir brauchen.

Wenn wir nur `_*` benutzen funktioniert das Matching, ein `x*` dagegen verurscht den Compilerfehler. Hier müssen wir also wieder zu `x @ _*` greifen.

Man kann mit Pattern Matching noch ein paar andere Sachen machen. Welche das sind und vor allem wie wir uns selbst eigene Extraktoren bauen können werden ich euch später erklären, nachdem wir die Objektorientierung hinter uns haben. Eigene Extraktoren würden uns z.B. erlauben so etwas zu schreiben:

val Person(name, address @ Address(city, zip, street)) = ...

Hier haben wir die zwei Klassen Person und Address und wollen an deren Inhalt rankommen. Sieht interessant aus, oder?

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.

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 -&gt; Germany, en -&gt; England, fr -&gt; France)

Um einen Wert aus einer Map zu erhalten gibt es zwei Möglichkeiten:

scala&gt; m("de")
res144: java.lang.String = Germany

scala&gt; 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&gt; 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.

Rechts-assoziative Methoden

Rechts-assoziative Methoden dürften allgemein bekannt sein. Darunter fallen alle Methoden, die mit einem Doppelpunkt enden und in Infix- bzw. Operator-Position stehen:

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

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

So weit so gut. Was allgemein weniger bekannt sein dürfte ist, dass das nur für Methoden mit einem Parameter funktioniert:

scala> class X { def -: (i: Int, s: String) {} }
defined class X

scala> new X
res20: X = X@6a2a9a0e

scala> (0, "") -: res20
<console>:11: error: not enough arguments for method -:: (i: Int, s: String)Unit.
Unspecified value parameter s.
              (0, "") -: res20
                      ^

Der Grund warum dies nicht funktioniert finden wir in der specin Section 6.12.3:

A left-associative binary operation e1 op e2 is interpreted as e 1.op(e2). If op is right-associative, the same operation is interpreted as { val x =e1; e2.op(x) }, where x is a fresh name.

Dies bedeutet, dass unser Code in etwa zu diesem transformiert wird:

{
  val x = (0, "")
  res99.-:(x)
}

Wie zu erkennen haben wir plötzlich keine zwei Parameter mehr, sondern einen Tuple2. Das passt nicht mehr. Anders herum ist es aber kein Problem:

scala> println(1, "hello")
(1,hello)

Wir können einen Tuple erstellen, ohne dass wir ihn in extra Klammern schreiben müssen, stattdessen reicht das Klammerpaar der Parameterliste aus.

Teil 6.1: Seq

Die in Scala wohl am meisten benutzte Collection ist Seq bzw. die Implementierung List. Da List deutlich mehr Operationen als Seq unterstützt, wird meistens direkt mit List gearbeitet anstatt den Obertyp zu verwenden. Erstellt werden sie ganz einfach über ihren Typnamen:

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

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

Wie man anhand des REPL-Outputs erkennen kann erstellt sowohl Seq als auch List eine List (rechte Seite der Zuweisung). Dies wird auch der Runtime-Typ genannt. Dem Compiler ist dieser Typ nicht bekannt, er kann lediglich mit dem statischen Type arbeiten (linke Seite der Zuweisung). Innerhalb der REPL können wir sowohl den Runtime als auch den statischen Typ sehen, sobald wir unseren Code aber mit scalac übersetzen wissen weder wir noch der Compiler mit was für genauen Runtime-Typen gearbeitet wird.

In den eckigen Klammern steht der generische Typ eines Objekts, in diesem Fall Int. Durch die Typinferenz ist es aber unnötig ihn explizit bei der Objektkonstruktion mit anzugeben:

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

Gehen wir nun auf ein paar der wichtigsten Methoden ein. Zuallererst die Methode zur Größenermittlung einer Collection:

scala> xs.size
res57: Int = 3

Elemente hinzufügen:

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

scala> xs ++ Seq(4, 5, 6)
res64: Seq[Int] = List(1, 2, 3, 4, 5, 6)

Die `:+`-Methode fügt ein einzelnen Element am Ende hinzu, wohingegen `++` mehrere Elemente hinzufügen kann. Bitte beachtet, dass die Grundliste `xs` nie geändert wird – es wird immer eine neue Liste erstellt. Dass die Listen unveränderlich sind können wir zwar nicht ändern (außer auf die veränderlichen Collections zurückzugreifen), wir können aber die Variable ändern:

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

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

scala> xs = xs ++ Seq(5, 6, 7)
xs: Seq[Int] = List(1, 2, 3, 4, 5, 6, 7)

Die Methoden um den Inhalt auf den die Variablen zeigen zu ersetzen heißen genau gleich wie ihre unveränderlichen Pendants. Der einzige Unterschied ist, dass die neu zurückgegebene Collection nun an die Variable gebunden werden kann. Wie bereits aus Java bekannt gibt es die Möglichkeit die Zuweisungen abzukürzen indem man sie einfach vor das Gleichheitszeichen schreibt:

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

scala> xs :+= 4

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

scala> xs ++= Seq(5, 6, 7)

scala> xs
res105: Seq[Int] = List(1, 2, 3, 4, 5, 6, 7)

Dies ist bei allen Operation möglich, die Variableninhalte verändert. Vielleicht schaut ihr nebenbei die Dokumentation der Methoden, die Variableninhalte verändern können, in der API an, wenn nicht solltet ihr dies jetzt nachholen. Fällt euch was auf? Genau, sie existieren dort überhaupt nicht. Wie kann das sein? Die Methoden wurden keineswegs von dem Dokumentationswerkzeug scaladoc übergangen, es gibt nur eine spezielle Regel, die es überflüssig macht diese Methode tatsächlich zu implementieren: Immer dann wenn das letzte Zeichen einer Methode ein Gleichheitszeichen ist, dann prüft der Compiler als erstes ob diese Methode existiert. Falls dies nicht der Fall ist, dann transformiert er die Methode von

<collection> <op>= <obj>

in

<collection> = <collection> <op> <obj>

Sollte diese Methode ebenfalls nicht existieren, dann gibt der Compiler eine Fehlermeldung aus, ansonsten wird die Methode ausgeführt. Noch ein kleines Beispiel um die Transformation zu verdeutlichen. Wir wollen

xs ++= Seq(5, 6, 7)

ausführen. Die Methode `++=` existiert laut API aber nicht, also transformiert der Compiler das Statement zu

xs = xs ++ Seq(5, 6, 7)

Die Methode `++` kann der Compiler finden, weshalb der Code kompiliert. Sollte `xs` nicht mit `var` sondern mit `val` definiert worden sein, dann würde der Compiler eine Fehlermeldung ausgeben, da er zwar die richtige Methode gefunden hat, die Zuweisung aber nicht ausführen kann:

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

scala> xs ++= Seq(5, 6, 7)
:10: error: reassignment to val
              xs ++= Seq(5, 6, 7)
                 ^

Neben den Methoden, mit denen man Elemente an eine Collection hinten dran hängen kann, gibt es auch Methoden, mit denen man Objekte an den Anfang der Collections ergänzen kann. Laut API wird die Methode `+:` genannt, sie ist vom Namen her gespiegelt zur Methode `:+`, die ein Objekt an das Ende einer Collection schreibt. Nur irgendwie seltsam, dass wenn wir diese Methode eingeben wir eine Fehlermeldung erhalten:

scala> xs +: 4
:10: error: value +: is not a member of Int
              xs +: 4
                 ^

Was funktioniert da jetzt schon wieder nicht? Wir sind hier wieder auf eines der „geheimen“ Features von Scala gestoßen, die auf den ersten Blick total verwirren. Für das Verhalten gibt es wieder eine Regel: Endet eine Methode mit einem Doppelpunkt, so wird der Parameter mit dem Objekt, auf das die Methode aufgerufen wurde vertauscht. Diese Methoden werden rechts-assoziativ genannt, da sie auf das ihnen rechts stehende Objekt angewendet werden. Der aufgerufene Code

<obj> <op>: <param>

wird zu

<param> <op>: <obj>

Am Beispiel von `+:` bedeutet dies, dass der Aufruf von

xs +: 4

zu

4 +: xs

transformiert wird. Deshalb erhalten wir auch eine Fehlermeldung, da der Typ Int keine Methdode namens `+:` besitzt. Damit der Code erfolgreich kompiliert müssen wir die Codeteile „umdrehen“:

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

Ein Element an den Beginn einer Collection mag für euch vielleicht noch nicht viel Sinn machen, es gibt aber genug Anwendungsfälle, bei denen man dieses Verhalten gut gebrauchen kann. Wo das genau der Fall ist werde ich bald erklären.

Der Zugriff auf die Elemente einer Seq ist wie vieles andere ebenfalls besonders einfach:

scala> xs(0)
res66: Int = 1

scala> xs(3)
java.lang.IndexOutOfBoundsException: 3

Der Index eines Elements wird einfach in Klammern hinter die Collection gesetzt – das wars. Der erste Index aller Typen von Seq ist dabei immer 0. Falls der Index größer gleich als `seq.size` oder negativ ist fliegt eine Exception.

In Scala sind Methoden mit Operatornamen innerhalb der Collections bevorzugt. Methoden wie `add`, `addAll` oder `get` gibt es daher nicht (zumindest nicht bei Seq). Weitere Operationen:

scala> xs.contains(1)
res72: Boolean = true

scala> xs contains 1
res73: Boolean = true

Bitte ruft euch in Erinnerung, dass auch Methoden ohne Operatornamen in `operator notation` aufgerufen werden können. Besonders wichtig sind auch die Methoden um bestimmte Elementgruppen einer Collection zurückzugeben:

scala> xs.head
res74: Int = 1

scala> xs.tail
res75: Seq[Int] = List(2, 3)

scala> xs.last
res76: Int = 3

scala> xs.init
res77: Seq[Int] = List(1, 2)

`head` gibt den Kopf (das erste Element zurück), `tail` gibt alle Elemente bis auf das Erste zurück, `last` gibt das letzte Element zurück und `init` gibt alle Elemente bis auf das Letzte zurück.

Es gibt noch viele weitere nützliche Methoden, ich möchte diese hier aber nicht zu ausführlich auflisten. Gehen wir lieber weiter zu den anderen Collectiontypen und deren unterschiedliche Handhabung.

List

Wie ich euch bereits am Anfang des Artikel erklärt habe besitzt List viele Methoden, die Seq nicht zur Verfügung stehen. Damit wir auf diese Methoden zugreifen können reicht es nicht nur mit dem Runtime-Typ List zu arbeiten, wir müssen ihn auch bei unserem statischen Typ benutzen. Durch Scalas Typinferenz ist das aber überhaupt kein Problem, da wir so selten die genauen Typen spezifizieren müssen. Wir sollten nur aufpassen, dass die Schnittstellen unserer Objekte keine List sondern eine Seq zurückgeben – wir wollen ja gegen die Schnittstelle programmieren und nicht gegen die Implementierungen. Im Folgenden möchte ich euch die Unterschiede zu Seq aufzeigen.

Eine List zu erstellen ist denkbar einfach, es genügt

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

zu schreiben. Diese Schreibweise kennt ihr bereits. Was ihr noch nicht kennt ist das Folgende:

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

Die `::`-Methode konkateniert die Elemente mit einer Liste. Der Typ `Nil` ist ebenfalls eine List, genau genommen ist es ein Singleton-Objekt, das von List erbt und eine leere Liste repräsentiert. Wir könnten anstatt Nil auch `List()` schreiben, dies würde auch eine leere List erzeugen. Nil ist aber weniger zu schreiben und dabei genauso verständlich. Dadurch, dass `::` auf einen Doppelpunkt endet wird die Aufrufereihenfolge umgedreht, der Compiler würde also folgenden Code erkennen:

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

Dies wäre aber lange nicht so gut lesbar wie die erstgenannte Variante. Der Code funktioniert übrigens, da die rechts-assoziative Methode links-assoziativ geparst wird. Das bedeutet, man könnte den Code auch

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

schreiben. Er wird von rechts nach links aufgelöst, deshalb links-assoziativ. Würden wir nur `1 :: 2` schreiben würden wir eine Fehlermeldung erhalten, da Int keine Methode `::` kennt. Durch die links-Assoziativität hingegen wird nach Auflösung jeder Klammer immer eine List zurückgegeben, auf die `::` angewendet werden kann:

scala> 3 :: Nil
res15: List[Int] = List(3)

scala> 2 :: res15
res16: List[Int] = List(2, 3)

scala> 1 :: res16
res17: List[Int] = List(1, 2, 3)

Das ist die gesamte „Magie“ hinter `::`. Die Methode birgt jetzt schon den ersten Anwendungsfall bei dem es Sinn macht Objekte an den Beginn einer List zu hängen und nicht zum Ende.

List besitzt noch die Methode `:::` die gleich wie `++` funktioniert, die Elemente aber wie `::` an den Anfang der List schreibt:


scala> xs ::: List(4, 5, 6)
res22: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> xs ++ List(4, 5, 6)
res23: List[Int] = List(1, 2, 3, 4, 5, 6)

Die Ausgabe ist zwar identisch, die Auswertungsreihenfolge ist aber eine andere, da `:::` rechts-assoziativ ist (endet mit einem Doppelpunkt). Wir können uns vergewissern, dass dies so ist wenn wir die Methode in `object notation` aufrufen:

scala> xs.:::(List(4, 5, 6))
res25: List[Int] = List(4, 5, 6, 1, 2, 3)

scala> xs.++(List(4, 5, 6))
res26: List[Int] = List(1, 2, 3, 4, 5, 6)

Sobald wir dies tun gilt die Regel mit der rechts-Assoziativität nicht mehr und man kann deutlich erkennen wie die Elemente zum Beginn hinzugefügt werden. Wenn wir mit List arbeiten ist das Voranstellen von Elementen die bevorzugte Variante, da das Anhängen von Elementen mehr Zeit in Anspruch nimmt. Die Implementierung von List entspricht einer einseitig verlinkten Liste, die Variable, die die List aufnimmt zeigt dabei immer auf den Kopf der Liste. Anhängen von Elementen würde also bedeuten, dass die komplette Liste erst durchlaufen werden muss bevor am Ende ein Element hinzugefügt werden kann. Dies entspricht einer Laufzeit von O(n) wobei n gleich der Anzahl der Elemente. Wenn wir Elemente voranstellen haben wir eine Laufzeit von O(1), da das neue Element direkt zum neuen Kopf der List wird (xs.head) und auf den Rest (xs.tail) zeigt. Wollen wir Elemente von einer bestimmten Position lesen müssen wir die Liste wieder umständlich durchlaufen, List eignet sich also besonders wenn wir sie komplett durchlaufen wollen. Bei einem indexbasierten Zugriff sollten wir also zu einer IndexedSeq greifen.

IndexedSeq

Die beiden wichtigsten Vertreter der indexbasierten Collections sind Array und Vector. Im Gegensatz zu List sind dies keine verketteten Listen sondern speichern die Daten indexbasiert ab. Dies bedeutet, dass wir an jedes Element mit einer Laufzeit von O(1) gelangen können. Der Unterschied zwischen Array und Vector ist, dass sich die Elemente eines Vectors nicht ändern können, die eines Arrays hingegen schon. Bei beiden gilt aber weiterhin, dass sich die Anzahl der Elemente nach der Konstruktion nicht mehr ändern lassen. Die Standardimplementierung von IndexedSeq ist Vector weshalb wir ihn nie direkt über die Konstruktoren von Vector erzeugen brauchen:

scala> val xs = IndexedSeq(1, 2, 3)
xs: IndexedSeq[Int] = Vector(1, 2, 3)

scala> xs(0) = 4
:10: error: value update is not a member of IndexedSeq[Int]
              xs(0) = 4
              ^

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

scala> xs(0) = 4

scala> xs
res43: Array[Int] = Array(4, 2, 3)

Vector basiert intern auf einem Array und sorgt dafür, dass sich dessen Elemente nicht ändern lassen. Je nach Anforderung sollte also die eine oder die andere Collection genommen werden. Array zählt eigentlich zu den veränderlichen Collections, die ich nochmal getrennt besprechen möchte. Da das Array aber direkt von der JVM unterstützt wird ist es die schnellste und speichersparendste Collection weshalb ich sie hier auch erwähne.

Zu beachten ist, dass selbst ein Array mit runden Klammern adressiert wird. Die Schreibweise mit den eckigen Klammern funktioniert nicht.

String

Strings gehören in Scala ebenfalls zu den Collections. Wir können auf sie genau die gleichen Methoden aufrufen wie auch bei den anderen Collections:

scala> val str = "8735421960"
str: java.lang.String = 8735421960

scala> str.size
res46: Int = 10

scala> str(0)
res47: Char = 8

scala> str.sorted
res48: String = 0123456789

Ein String ist dabei gleichzusetzen mit einer Seq[Char]:

scala> val str: Seq[Char] = "8735421960"
str: Seq[Char] = 8735421960

String basiert dabei auf der String-Repräsentation der JVM, ist gleichzeitig aber auch eine Collection. Dies ermöglicht uns eine elegante Stringverarbeitung, ohne dass wir wie in Java auf irgendwelche Helper-Klassen zugreifen müssten. Wie genau die Brücke zwischen JVM-String und Scala-Collection geschlagen wird werdet ihr noch früh genug erfahren. Dass implizite Konvertierungen dabei eine Rolle spielen solltet ihr schon wissen wenn ihr die anderen Artikel aufmerksam gelesen habt. Welche Methoden von String genau unterstützt werden erfahrt ihr wenn ihr in die Dokumentation von WrappedString schaut.

Range

Eine besonders tolle Collection ist die Range. Sie ermöglichen uns in Verbindung mit der for-Expression elegante Iterationen über Collections. Schauen wir uns einmal die eleganteste Schreibweise zur Erstellung einer Range an:

scala> 1 to 5
res49: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)

Einfach. Praktisch. Gut. Mehr gibt es dazu eigentlich nicht zu sagen. `to` ist im übrigen kein Schlüsselwort, sondern eine Methode:

scala> 1.to(5)
res50: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)

Sie erstellt eine Range, die vom Startwert (1) bis zum Endwert (5) geht. Neben `to` gibt es auch noch `until`, bei dem der letzte Wert nicht inklusive, also ausgelassen wird:

scala> 1 until 5
res51: scala.collection.immutable.Range = Range(1, 2, 3, 4)

Mit der Methode `by` lässt sich die Schrittweite der Range festlegen:

scala> 1 to 10 by 2
res52: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9)

scala> 10 to 1 by -2
res53: scala.collection.immutable.Range = Range(10, 8, 6, 4, 2)

Dies erlaubt uns sowohl vorwärts als auch rückwärts zu gehen. Als Übersichtlichkeitsgründen werde ich mich in einem gesondertem Kapitel darauf konzentrieren wie die for-Expressions mit den Ranges zusammenarbeiten.

Iterator

Wir haben jetzt viele Möglichkeiten kennen gelernt wie wir Collections erstellen können, aber die Collection bringt uns nichts wenn wir nicht mit Schleifen auf ihre Elemente zugreifen können. Die naheliegendste Lösung wäre eine while-Schleife zu benutzen:

def printList(xs: List[Int]) {
  var i = 0
  while (i < xs.size) {
    println(xs(i))
    i += 1
  }
}

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

scala> printList(xs)
1
2
3

Nur leider ist das nicht besonders elegant. Der Iterator, bietet eine weitere Möglichkeit über die Elemente zu iterieren.

def printList(xs: List[Int]) {
  val i = xs.iterator
  while(i.hasNext)
    println(i.next)
}

scala> printList(xs)
1
2
3

Über die Methode `iterator` bekommen wir von jeder Collection einen Iterator. Die Methoden `hasNext` und `next` sprechen für sich.

Die Methode `mkString` macht es übrigens besonders einfach eine Collection in einen String umzuwandeln:

scala> println(xs.mkString("\n"))
1
2
3

scala> println(xs.mkString("[", ", ", "]"))
[1, 2, 3]

Wofür die Parameter genau stehen verrät die API oder die REPL mit ihren auto-completion Fähigkeiten.

Bitte beachtet, dass ein Iterator nur ein mal durchlaufen werden kann. Ist der Iterator am Ende angekommen gibt es keine Möglichkeit mehr ihn zu „resetten“. Stattdessen muss ein neuer Iterator erstellt werden. Der Iterator gehört zu den `lazy collections` da er die Elemente erst dann berechnet bzw. auf sie zugreift wenn sie gebraucht werden. In die gleiche Kategorie fallen View und Stream, wobei sich die drei grundlegend voneinander unterscheiden. Ein Iterator kennt immer nur ein Element, nämlich das über das er gerade iteriert – er ist also mehr ein Pointer auf ein Element als ein eigene Collection.

View

Eine View unterscheidet sich vom Iterator hauptsächlich darin, dass sie eben schon eine eigene Collection darstellt. Im Gegensatz zum Iterator, den nur einmal aufgerufen werden kann, darf man eine View beliebig oft aufrufen. Die Elemente werden dabei bei jedem Zugriff neu berechnet, sie werden also nicht gespeichert. Sowohl View als auch Iterator benötigen also also immer nur Speicherplatz für ein Element. Views kann man mit der Methode `view` erstellen:

scala> xs.view
res60: java.lang.Object with scala.collection.SeqView[Int,List[Int]] = SeqView(...)

Hier noch ein kleines Beispiel, das den Unterschied zwischen View und Iterator erläutert:

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

scala> val i = xs.iterator
i: Iterator[Int] = non-empty iterator

scala> i.drop(2).next
res70: Int = 3

scala> i.drop(2).next
java.util.NoSuchElementException: next on empty iterator

scala> val v = xs.view
v: java.lang.Object with scala.collection.SeqView[Int,List[Int]] = SeqView(...)

scala> v.drop(2).head
res72: Int = 3

scala> v.drop(2).head
res73: Int = 3

Wie ganz klar zu erkennen wirft der Iterator eine Exception wenn man versucht auf ein Element zuzugreifen, das nicht existiert. `drop` versucht die ersten n Elemente zu löschen und da die Liste nur drei Elemente besitzt können wir keine zwei mal zwei Elemente löschen und dann auf ein nachfolgendes Element zugreifen. Da die View hingegen immer wieder eine neue Collection erzeugt können wir den Löschvorgang beliebig oft wiederholen ohne einen Fehler zu bekommen.

Stream

Die letzte Kategorie der `lazy collections` bildet der Stream. Er erzeugt seine Elemente ebenfalls erst auf Anfrage speichert bereits berechnete Elemente aber ab um im Bedarfsfall schnell wieder darauf zugreifen zu können. Der größte Vorteil von Streams ist unendlich lange Collections zu erzeugen, gegebenfalls auch rekursiv.

scala> val s = Stream from 1
s: scala.collection.immutable.Stream[Int] = Stream(1, ?)

scala> s(0)
res80: Int = 1

scala> s(1)
res81: Int = 2

scala> s(1000)
res82: Int = 1001

scala> val s = Stream.from(1, 3)
s: scala.collection.immutable.Stream[Int] = Stream(1, ?)

scala> s(0)
res87: Int = 1

scala> s(1)
res88: Int = 4

scala> s(10)
res89: Int = 31

Streams können schwer zu verstehende Gebilde sein, vor allem wenn sie rekursiv erzeugt werden. Wenn die Zeit reif ist werden wir uns das mal genauer anschauen.

Um von einer `lazy collection` (View, Stream) wieder zurück zu einer `strict collection` (die Collections, die ihre Elemente alle auf einmal berechnen) zu kommen reicht es die Methode `force` aufzurufen:

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

scala> val v = xs.view
v: java.lang.Object with scala.collection.SeqView[Int,List[Int]] = SeqView(...)

scala> v.force
res93: List[Int] = List(1, 2, 3)

`force` sollte nicht auf einen unendlichen Stream aufgerufen werden, da sonst immer neue Elemente bis zum Absturz eures Programms berechnet werden.