Game of Life mit Scala - das Tutorial - JAXenter

Game of Life mit Scala – das Tutorial

Klassenparameter, Felder und Methoden

Da unsere Zellen auf einem Spielfeld platziert sind, wollen wir die Klasse Cell um die Koordinaten des jeweiligen Gitterquadrats erweitern. Diese schreiben wir erst einmal als so genannte Klassenparameter in Klammern hinter den Klassennamen: class Cell(x: Int, y: Int). Das sieht jetzt aber ganz anders aus als in Java! Erstens kennt Java keine Klassenparameter und zweitens notieren wir in Scala jegliche Parameter wie in UML, d. h. zuerst der Name, dann ein Doppelpunkt und abschließend der Typ. Wozu dienen eigentlich Klassenparameter? Aus diesen ergibt sich implizit der so genannte Primary Constructor, sodass wir neue Zellen nun nur noch folgendermaßen erzeugen können:

scala> new Cell(1, 2)
res0: com.weiglewilczek.gameoflife.Cell = com...

Die Klassenparameter entsprechen privaten Feldern. Wir können also weder von außen auf diese zugreifen, noch können wir unsere Zelle ändern. Ersteres wollen wir später noch so modifizieren, dass wir lesenden Zugriff haben. Das Konzept der Unveränderlichkeit ist jedoch in Scala und ganz allgemein in der funktionalen Programmierung sehr prominent. Sogar in Java sind Immutable Objects nichts Neues und laut Effective Java [9] grundsätzlich veränderlichen Klassen vorzuziehen.

Unveränderliche Felder definieren wir in Scala mit dem Schlüsselwort val und veränderliche mit var. Wir legen jetzt ein privates unveränderliches Feld an, das die Position der Zelle als String wiedergibt: private lazy val position = „(%s, %s)“.format(x, y).

Huch, wo ist die Typangabe geblieben? Die brauchen wir nicht, weil der Scala-Compiler anhand des Werts, der zugewiesen wird, erkennen kann, dass position ein String sein soll. Diese so genannte Type Inference, die auch für Methoden und generische Typen funktioniert, macht Scala-Code sehr leichtgewichtig, obwohl er statisch typisiert ist.

Mit dem Schlüsselwort lazy legen wir übrigens fest, dass die Zuweisung und damit auch die Auswertung der „rechten Seite“ erst dann erfolgt, wenn auf das Feld zugegriffen wird. Das geschieht bisher noch gar nicht, denn position ist ja ein privates Feld, das nirgendwo innerhalb von Cell verwendet wird. Aber ein Blick auf obige Ausgaben der REPL legt nahe, dass wir dieses Feld nutzen könnten, um die toString-Methode zu überschreiben. Da unsere Zelle unveränderlich ist, können wir hier auch diese unveränderliche Position verwenden: override def toString = position.

Wie wir sehen, definieren wir in Scala Methoden mit dem Schlüsselwort def . Wenn wir konkrete Methoden oder Felder überschreiben, dann müssen wir das Schlüsselwort override verwenden, das wir optional auch beim Implementieren von abstrakten Methoden oder Feldern anwenden können. Wir setzen hier wieder auf Type Inference, lassen also die Typangabe weg. Bei nicht-trivialen öffentlichen Methoden, bei denen nicht unmittelbar vom „Hinschauen“ klar ist, welchen Typ sie zurückgeben, ist es jedoch guter Stil, den Typ anzugeben. Hier sähe das folgendermaßen aus: override def toString: String = position.

Auch mit der Typangabe ist die Notation noch deutlich leichtgewichtiger als in Java: Erstens können wir bei Einzeilern die geschweiften Klammern weglassen. Zweitens brauchen wir kein Schlüsselwort return, denn in Scala dient das Ergebnis der letzten Zeile eines Codeblocks – hier ohnehin nur eine einzige Zeile – als Rückgabewert. Wir werden das im weiteren Verlauf noch anhand von komplexeren Methoden sehen. Nun wollen wir doch gleich einmal ausprobieren, wie sich diese Änderungen auswirken:

scala> new Cell(1, 2)
res0: com.weiglewilczek.gameoflife.Cell = (1, 2)

Sehr schön, nun erhalten wir von der REPL eine aussagekräftige Ausgabe. Allerdings können wir immer noch nicht auf die Koordinaten zugreifen, denn die Klassenparameter sind ja privat. Deshalb machen wir diese einfach zu öffentlichen Feldern, indem wir das Schlüsselwort val voranstellen: class Cell(val x: Int, val y: Int) { …. Zum Beweis wieder der Ausflug in die REPL, wobei wir hier erstmalig auch in der REPL mittels val den sprechenden Variablennamen cell zum weiteren Zugriff definieren:

scala> val cell = new Cell(1, 2)
cell: com.weiglewilczek.gameoflife.Cell = (1, 2)
scala> cell.x
res0: Int = 1
Case Classes

Jetzt wollen wir uns das Leben noch ein wenig einfacher machen: Mit dem Schlüsselwort case vor der Klassendefinition machen wir unsere Zelle zur Case Class und bekommen vom Scala-Compiler dadurch unter anderem folgende Eigenschaften „geschenkt“:

  • Klassenparameter sind automatisch vals, sodass wir das Schlüsselwort val wieder entfernen können
  • Instanzen können ohne das Schlüsselwort new erzeugt werden; die Erklärung dazu führt hier leider zu weit
  • Die Case Class bekommt „vernünftige“ Implementierungen für equals und hashCode; eigentlich auch für toString, aber diese Methode überschreiben wir ja

Der komplette Code für unsere Zelle nach all diesen Änderungen ist in Listing 1 dargestellt. Zur Überprüfung der gerade aufgezählten Eigenschaften begeben wir uns wieder in die REPL:

scala> val cell = Cell(1, 2)
cell: com.weiglewilczek.gameoflife.Cell = (1, 2)
scala> Cell(1, 2) equals Cell(1, 2)
res0: Boolean = true
scala> Cell(1, 2) equals Cell(1, 1)
res1: Boolean = false
Listing 1: Cell.scala
package com.weiglewilczek.gameoflife
case class Cell(x: Int, y: Int) {
  override def toString = position
  private lazy val position = "(%s, %s)".format(x, y)
}
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.