Game of Life mit Scala – das Tutorial

For Comprehensions

Didaktisch ein wenig früh, aber wegen des Beispiels schon jetzt erforderlich, kommen wir nun zu einem sehr interessanten Sprachkonstrukt: Zu den „For Comprehensions“. An dieser Stelle sei hervorgehoben, dass in Scala, wie in der funktionalen Programmierung üblich, quasi alles ein Ergebnis hat. So gibt es z. B. wie in Java ein if-else, aber dieses steuert nicht nur den Kontrollfluss, sondern hat ein Ergebnis, das einer Variablen zugewiesen oder als Rückgabewert einer Methode verwendet werden kann. Somit entspricht das if-else in Scala eigentlich dem Conditional-Operator (x ? y : z) in Java.

Nun zu den For Comprehensions. Diese entsprechen in ihrer einfachsten Form Schleifen: for (i <- 1 to 10) { … }. Dann haben sie ausnahmsweise keinen Rückgabewert und dienen nur dazu, einen Seiteneffekt auszuüben. Übrigens ist to kein Schlüsselwort, sondern eine Methode, die durch so genannte Implicit Conversions, ein ziemlich fortgeschrittenes Feature, u. a. auch auf Int-Werte angewendet werden kann und eine Collection vom Typ Range zurückgibt. In ihrer funktionalen Form enden For Comprehensions mit dem Schlüsselwort yield und haben ein Ergebnis. Ein ganz einfaches Beispiel:

scala> for (i 

Wir sehen, dass eine Collection (Range von 1 bis 10) auf eine andere (Range 2 bis 11) abgebildet wird. Wir wollen nun For Comprehensions einsetzen, um die Nachbarn einer Zelle zu ermitteln:

def neighbours: Traversable[Cell] =
  for {
    i 

OK, was bedeutet das? Von oben nach unten:

  • Wir geben den Typ (siehe Collections weiter unten) der Methode explizit an, weil wir keinen trivialen Einzeiler vor uns haben; Typparameter definieren wir in Scala übrigens mit eckigen Klammern
  • Wir verwenden geschweifte Klammern statt runder; dadurch können wir mehrere Zeilen schreiben, ohne einen Strichpunkt zu verwenden
  • Wir verwenden zwei so genannte Generators, um alle benachbarten x- und y- Koordinaten um unsere Zelle herum zu durchlaufen
  • Wir wenden einen Filter an, um die Zelle selbst auszuschließen
  • Mittels yield erzeugen wir die Nachbarzellen

Der komplette Code für unsere Zelle nach dieser Ergänzung ist in Listing 2 dargestellt. In der REPL erhalten wir wie erwartet eine Collection mit den acht Nachbarn der gegebenen Zelle:

scala> cell.neighbours      
res0: Traversable[...Cell] = Vector((0, 1), (0, 2), (0, 3), ...)
Listing 2: Cell.scala
package com.weiglewilczek.gameoflife
case class Cell(x: Int, y: Int) {
  def neighbours: Traversable[Cell] =
    for {
      i 
Unit Testing mit specs

Code in der REPL auszuprobieren gibt uns vielleicht ein gutes Gefühl, aber selbstverständlich sollten wir auch in Scala systematisch testen. Hierfür könnten wir natürlich JUnit verwenden, aber es gibt einige Scala-Testwerkzeuge, die es uns ermöglichen, bessere weil besonders sprechende Tests zu erstellen. Hier wollen wir das Framework specs [10] verwenden: Dazu laden wir uns die passende Version 1.6.5 für Scala 2.8.0 (specs_2.8.0-1.6.5.jar) herunter und legen das JAR in das lib-Verzeichnis unseres Projekts. Nun legen wir im Testverzeichnis src/test/scala die Scala-Klasse CellSpec an. Wir verwenden dasselbe Package wie für Cell und erweitern die specs-Klasse Specification. Im Rumpf der Klasse schreiben wir den folgenden Testcode:

"A Cell" should {
  "have eight neighbours" in {
    Cell(0, 0).neighbours must haveSize(8)
  }
}

Das ist gültiger Scala-Code? Ja! Auch hier kommen wieder Implicit Conversions ins Spiel, sodass die Methoden should und in auf den Strings bzw. must auf dem Ergebnis von neighbours aufgerufen werden können. Das Resultat ist ein sehr verständlicher, sich selbst dokumentierender Test.

Wir wollen hier keine vollständige Testabdeckung erreichen, sondern abschließend zeigen, wie wir diesen Test mit SBT ausführen können. Dazu geben wir in der SBT-Konsole test ein, wodurch zunächst alle Tests kompiliert und anschließend ausgeführt werden. Wie wir sehen, werden die Texte, mit denen wir den Test beschrieben haben, beim Testreport verwendet. Insbesondere fehlgeschlagene Tests, die wir hier kurz durch den Einbau eines Fehlers (z. B. Ranges in der For Comprehension abändern: i <- (x – 1 to x – 1) ) simulieren, führen so zu sehr verständlichen Fehlerbeschreibungen:

[info] A Cell should
[info]   x have eight neighbours
[info]     'Vector((-1, -1))' doesn't have size 8. It has size 1 ...
Fazit

Damit sind wir für heute am Ende angelangt. Wir haben erste Schritte mit Scala-Werkzeugen wie REPL und SBT gemacht und insbesondere die objektorientierte Seite von Scala kennengelernt. Im kommenden Teil werden wir vor allem die Möglichkeiten der funktionalen Programmierung behandeln. Selbstverständlich freuen wir uns über Rückmeldungen, sei es in Form von Fragen, Bug-Reports (bitte über den Issue Tracker [11]) oder Ideen, wie wir das Tutorial verbessern oder ausbauen können. Viel Spaß mit Scala!

Heiko Seeberger ist geschäftsführender Gesellschafter der Weigle Wilczek GmbH und verantwortlich für die technologische Strategie des Unternehmens mit den Schwerpunkten Java, Scala, OSGi, Eclipse RCP, Lift und Akka. Zudem ist er aktiver Open Source Committer, Autor zahlreicher Fachartikel und Redner auf einschlägigen Konferenzen.
Kommentare

Schreibe einen Kommentar

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