Archive for August 2011|Monthly archive page

Teil 13: Fallklassen

Im letzten Kapitel haben wir Extraktoren kennen gelernt. Ich habe euch bereits ein paar Möglichkeiten gezeigt wie man sie einsetzen kann, tatsächlich gibt es aber noch viel mehr Möglichkeiten wie man sie noch einsetzen kann. Durch ihre Hilfe ist es uns z.B. möglich komplexe Ausdrücke komfortabel auszuwerten, typsichere Aufzählungen zu kreieren oder Objekte miteinander kommunizieren zu lassen. Durch die Vielzahl an Möglichkeiten wie man sie einsetzen kann wurde Scala durch sogenannte Fallklassen (engl.: case class) aufgewertet, die „normale“ Klassen durch syntaktischen Zucker erweitern und uns die Arbeit erleichtern.

Schauen wir uns noch einmal das Personen-Beispiel an:

class Person(val name: String, val age: Int, val address: Address)
object Person {
  def unapply(p: Person) = Some(p.name, p.age, p.address)
}
class Address(val city: String, val zip: Int, val street: String)
object Address {
  def unapply(a: Address) = Some(a.city, a.zip, a.street)
}

Wir mussten wir für unsere Klassen die Extraktoren erzeugen. Wenn wir wollten, könnten wir auch noch eine apply-Methode erstellen. Es gibt jedoch viel zu viele Anwendungsfälle bei denen wir von Extraktoren Gebrauch machen können, folglich haben wir unnötig viel Code-Duplikation. Genau aus diesem Grund gibt es Fallklassen, die denkbar einfach zu erstellen sind.

case class Person(name: String, age: Int, address: Address)
case class Address(city: String, zip: Int, street: String)

Es genügt das Schlüsselwort case vor die Klasse zu schreiben. Schauen wir uns gleich mal an, was uns das bringt:

scala> val p = Person("franz", 43, Address("gornau", 12345, "rabenweg"))
p: Person = Person(franz,43,Address(gornau,12345,rabenweg))

scala> val Person(name, age, address @ Address(city, zip, street)) = p
name: String = franz
age: Int = 43
address: Address = Address(gornau,12345,rabenweg)
city: String = gornau
zip: Int = 12345
street: String = rabenweg

scala> p.name
res0: String = franz

scala> p.address.zip
res1: Int = 12345

Wie zu erkennen entfällt die Erstellung einer apply- und unapply-Methode. Der Compiler erstellt für uns ein Companion-Objekt und erzeugt die genannten Methoden. Weiterhin implementiert er noch eine Reihe weiterer Methoden für unsere Fallklassen. Dazu gehören z.B. toString(), equals() und hashCode(). Erstere gibt uns eine ansehnliche Stringrepräsentation unserer Klasse zurück, die beiden anderen ermöglichen uns Objekte miteinander zu vergleichen.
Weiterhin erzeugt uns der Compiler auch noch Getter für unsere Attribute ohne dass wir dies explizit durch ein val angeben müssten.

Schauen wir uns noch an wie wir all diese Methoden nutzen können.

scala> class IntHolder(i: Int)
defined class IntHolder

scala> new IntHolder(17) == new IntHolder(17)
<console>:9: warning: comparing a fresh object using `==' will always yield false
              new IntHolder(17) == new IntHolder(17)
                                ^
res0: Boolean = false

scala> case class IntHolder(i: Int)
defined class IntHolder

scala> IntHolder(17) == IntHolder(17)
res1: Boolean = true

scala> IntHolder(17) == IntHolder(3)
res2: Boolean = false

Das Vergleichen unserer Objekte funktioniert perfekt. Der Compiler sorgt dafür, dass wir keine Fehler bei den Vergleichsmethoden machen können. So können wir Fallklassen auch in ein Set einfügen und sicher gehen, dass auch wirklich keine Duplikate enthalten sind:

scala> class IntHolder(i: Int)
defined class IntHolder

scala> Set(new IntHolder(23), new IntHolder(23))
res6: scala.collection.immutable.Set[IntHolder] = Set(IntHolder@b3f451d, IntHolder@66d278af)

scala> case class IntHolder(i: Int)
defined class IntHolder

scala> Set(IntHolder(23), IntHolder(23))
res7: scala.collection.immutable.Set[IntHolder] = Set(IntHolder(23))

Sehr schön ist auch die Erstellung einer copy-Methode, die es uns erlaubt unveränderliche Objekte komfortabel zu kopieren:

scala> case class X(i: Int, s: String)
defined class X

scala> val x1 = X(8, "hello")
x1: X = X(8,hello)

scala> val x2 = x1.copy(s = "world")
x2: X = X(8,world)

Durch benannte Argumente können wir nur das Attribut angeben, das geändert werden soll. Manchmal ganz nützlich ist eine Methode namens productIterator, mit der wir über alle Elemente unserer Klasse iterieren können:

scala> for (x <- x1.productIterator) println(x)
8
hello

Wenn das alles so einfach geht, warum habe ich euch dann im letzten Kapitel mit Extraktoren gequält? Der Grund ist keinesfalls meine Boshaftigkeit 😉 oder weil ich nach dem Motto „Warum einfach wenn es auch kompliziert geht?“ lebe, sondern einfach damit ihr es mal gesehen habt. In den meisten Anwendungsfällen werden Fallklassen vollkommen ausreichen, manchmal muss man aber eben doch noch selbst Hand anlegen und dann ist es nicht schlecht wenn man weiß was man machen muss.

Da es zu Fallklassen eigentlich nicht mehr viel zu sagen gibt wollen wir uns gleich ein paar Anwendugsgebiete anschauen.

Algebraische Datentypen (ADT)

Den Anfang macht erst einmal eine Erklärung zu ADTs. Oh Gott, das hört sich wieder kompliziert an, was ist das wieder für eine neue Teufelei? Ich kann euch beruhigen, ADT ist nur der Name eines Pattern, das ihr sogar schon kennen gelernt habt. Von einem ADT wird immer dann gesprochen wenn ein Datentyp die Gestalt eines Typs aus einer Menge von mehreren zusammengehörenden Typen ist. Hört sich kompliziert an? Für den Anfang ja, aber schauen wir uns ein Beispiel an und quälen uns nicht mit der Theorie herum:

data Bool = True | False

Betrachten wir den Typ, der mit data spezifiziert wird (hier Bool) als ein Typ, der die Formen True oder False annehmen kann (wobei der senkrechte Strich ein Oder symbolisiert). In Scala ist es leider nicht möglich ADTs so kurz und praktisch wie oben gezeigt zu notieren (obiger Code wäre valider Haskell-Code), durch Fallklassen hält sich der Overhead an Code aber in Grenzen:

abstract class Bool
case object True extends Bool
case object False extends Bool

Durch die Repräsentation des ADT mit einer geeigneten Vererbungshierarchie dürftet ihr es leichter haben euch das Typensystem vorzustellen. Wir haben einen abstrakten Obertyp, der durch mehrere Unterobjekte repräsentiert werden kann. In Scala besitzen wir die Möglichkeit durch ein object zu spezifizieren, dass ein Typ nur einmal existieren darf bzw. soll. Mehrere unterschiedliche True- oder False-Werte würden keinen Sinn machen, wir unterbinden also die Möglichkeit sie mehrfach zu erstellen. Den List-ADT habt ihr bereits kennen gelernt:

data IntList = Cons Int IntList | IntNil

abstract class IntList
case class Cons(head: Int, tail: IntList) extends IntList
case object IntNil extends IntList

ADTs sind in Scala lang nicht so schön zu implementieren wie z.B. in Haskell. Das liegt nicht nur daran, dass der syntaktische Overhead größer ist, sondern auch daran, dass wir jederzeit die Möglichkeit haben weitere Typen zu ergänzen. In Scala hindert uns niemand daran noch weitere Klassen zu erstellen, die von Bool erben. In Haskell wäre dies nach der Spezifizierung von Bool nicht mehr möglich. Der Grund warum wir nicht möchten, dass weitere Typen ergänzt werden ist Typsicherheit. Um zu verdeutlichen was für Probleme entstehen könnten schauen wir uns am besten folgendes Beispiel an:

def determine(b: Bool) = b match {
  case True => "true"
  case False => "false"
}

scala> determine(True)
res0: java.lang.String = true

scala> determine(False)
res1: java.lang.String = false

Durch Pattern Matching können wir bestimmen was für ein genauer Typ Bool zur Laufzeit hat. Wie zu erkennen besitzt die Methode keinen Default-Wert. Würden wir also einen anderen Typ als True oder False übergeben bekämen wir einen MatchError:

scala> case object X extends Bool
defined module X

scala> determine(X)
scala.MatchError: X (of class X$)
<stack trace>

In Haskell könnte der Code niemals fehl schlagen, da es ja keine Möglichkeit gibt nachträglich noch Typen zu ergänzen. In Scala müssen wir damit leben, dass uns der Compiler diese Typsicherheit nicht bieten kann. Falls also die Möglichkeit besteht, dass eines unserer Programme durch dieses Manko instabil bzw. manipuliert werden könnte, dann müssen wir wohl oder übel Default-Fälle einbauen.

Ein weiteres Beispiel für einen ADT wäre Int:

data Int = -2147483648 | ... | -1 | 0 | 1 | ... | 2147483647

Int kann als minimaler Wert -2³¹ und als maximaler Wert 2³¹-1 annehmen. Int wäre also immer ein Typ aus der Menge dieser Int-Literale.

Wenn wir die theoretische Seite der ADTs noch ein wenig genauer betrachten, dann stellen wir fest, dass ein ADT nur durch andere Typen der selben Menge aufgebaut werden kann und dass dessen Werte durch Pattern Matching extrahiert werden können. Was genau das bedeutet, soll uns wieder ein Beispiel erklären:

abstract class Shape
case class Point(x: Float, y: Float) extends Shape
case class Circle(p: Point, r: Float) extends Shape
case class Rectangle(p: Point, h: Float, w: Float) extends Shape

import math._

def area(s: Shape) = s match {
  case Point(_, _) => 0
  case Circle(_, r) => Pi*r*r
  case Rectangle(_, h, w) => h*w
}

def pointIntersectWith(p: Point, s: Shape) = p -> s match {
  case (Point(x1, y1), Point(x2, y2)) =>
    x1 == x2 && y1 == y2
  case (Point(x1, y1), Circle(Point(x2, y2), r)) =>
    (x1 >= x2-r && x1 <= x2+r) && (y1 >= y2-r && y1 <= y2+r)
  case (Point(x1, y1), Rectangle(Point(x2, y2), h, w)) =>
    (x1 >= x2 && x1 <= x2+w) && (y1 >= y2 && y1 <= y2+h)
}

In diesem Beispiel haben wir mehrere zweidimensional Figuren. Innerhalb der Methoden bauen wir uns die ADTs mit Hilfe der Extraktoren auseinander. Für alle Werte, die wir nicht benötigen, setzen wir den Unterstrich ein. Es ist sehr gut zu erkennen, dass einzelne Figuren auf anderen basieren. So benötigt ein Rechteck einen Punkt, der dessen Position im Koordinatensystem angibt. Testen wir den Code auf seine Funktionsweise:

scala> val c = Circle(Point(6, 7), 2)
c: Circle = Circle(Point(6.0,7.0),2.0)

scala> val r = Rectangle(Point(1, 2), 5, 3)
r: Rectangle = Rectangle(Point(1.0,2.0),5.0,3.0)

scala> area(c)
res5: Double = 12.566370614359172

scala> area(r)
res6: Double = 15.0

scala> pointIntersectWith(Point(1, 2), r)
res7: Boolean = true

scala> pointIntersectWith(Point(8, 8), c)
res8: Boolean = true

scala> pointIntersectWith(Point(8, 12), c)
res9: Boolean = false

Konstruktion und Extraktion erfordern eine nahezu identische Syntax, aber das ist uns bereits bekannt. Der Code funktioniert zwar, ist aber nicht besonders schön. Dank Scalas Objektorientierung können wir die benötigten Methoden auch in die ADTs verschieben:

abstract class Shape {
  def area: Float
  def intersectWith(s: Shape): Boolean
}
case class Point(x: Float, y: Float) extends Shape {
  def area = 0
  def intersectWith(s: Shape)= s match {
    case Point(x, y) =>
      this.x == x && this.y == y
    case Circle(Point(x, y), r) =>
      (this.x >= x-r && this.x <= x+r) && (this.y >= y-r && this.y <= y+r)
    case Rectangle(Point(x, y), h, w) =>
      (this.x >= x && this.x <= x+w) && (this.y >= y && this.y <= y+h)
  }
}
case class Circle(p: Point, r: Float) extends Shape {
  import math._
  def area = (Pi*r*r).toFloat
  def intersectWith(s: Shape) =
    throw new UnsupportedOperationException("circle.intersectWith")
}
case class Rectangle(p: Point, h: Float, w: Float) extends Shape {
  def area = h*w
  def intersectWith(s: Shape) =
    throw new UnsupportedOperationException("rectangle.intersectWith")
}

Die Objektorientierung erlaubt uns die Operator-Notation zu gebrauchen und den Code, der zu einem Objekt gehört, einfacher zu verwalten. Weiterhin gewinnen wir ein wenig Performance, da dank reduziertem Pattern Matching weniger Überprüfungen zur Laufzeit stattfinden müssen. Zwei der Methoden hab ich noch nicht implementiert und deshalb mit einer Exception versehen. Ich werde in einem späteren Artikel noch genauer auf Exceptions eingehen, momentan reicht es zu wissen, dass der String, der an die Exception übergeben wird, später im Stack-Trace stehen wird und dass die Exception mit dem Schlüsselwort throw aus der Methode geworfen wird. Exceptions sind in Scala Untertypen eines jeden Typs weshalb kein weiterer Rückgabetyp für die Methode angegeben werden muss.

scala> val r = Rectangle(Point(1, -2), 6, 3)
r: Rectangle = Rectangle(Point(1.0,-2.0),6.0,3.0)

scala> Point(3, 4) intersectWith r
res16: Boolean = true

scala> val c = Circle(Point(7, 3), 5)
c: Circle = Circle(Point(7.0,3.0),5.0)

scala> c.area
res17: Float = 78.53982

scala> r intersectWith c
java.lang.UnsupportedOperationException: rectangle.intersectWith

Der Code funktioniert wie erwartet und sollte keine weiteren Erklärungen mehr benötigen.

Ein Problem auf das man immer wieder stoßen kann, ist dass man vergisst auf bestimmte Typen zu prüfen. Die Wahrscheinlichkeit dafür steigt, umso größer die Menge an verfügbaren Typen ist. Scala stellt uns deswegen das Schlüsselwort sealed zur Verfügung, das den Compiler anweist zu überprüfen ob wir innerhalb der Pattern auch auf alle Typen überprüfen:

sealed abstract class Weekday
case object Mo extends Weekday
case object Tu extends Weekday
case object We extends Weekday
case object Th extends Weekday
case object Fr extends Weekday
case object Sa extends Weekday
case object Su extends Weekday

def isWeekend(w: Weekday) = w match {
  case Sa | Su => true
  case _ => false
}

def doTask(w: Weekday) = w match {
  case Sa | Su => "sleep very long"
  case Mo | Fr => "work less"
  case We | Th => "work hard"
}

Bei der Methode doTask erhalten wir vom Compiler eine Warnung:

<console>:17: warning: match is not exhaustive!
missing combination             Tu

       def doTask(w: Weekday) = w match {
                                ^
doTask: (w: Weekday)java.lang.String

Diese verschwindet erst nachdem wir für den Dienstag eine entsprechende Tätigkeit definiert haben. Obiges Beispiel ist eine Aufzählung, die wir in Scala anstatt von Enums verwenden können. Scala selbst bietet sowohl die Möglichkeit ADTs oder die klassischen Enums als Aufzählungstyp zu verwenden. Einen Enum erstellen wir indem wir ein Objekt von der Klasse Enumeration erben lassen und allen Aufzählungstypen den Value-Wert zuweisen:

object Weekday extends Enumeration {
  val Mo, Tu, We, Th, Fr, Sa, Su = Value
}

import Weekday._

def isWeekend(w: Weekday.Value) = w match {
  case Sa | Su => true
  case _ => false
}

scala> isWeekend(Tu)
res19: Boolean = false

scala> for(w <- Weekday.values) println(w)
Mo
Tu
We
Th
Fr
Sa
Su

scala> Weekday withName "Mo"
res25: Weekday.Value = Mo

Ob man sich nun für Enums oder für ADTs entscheidet hängt ganz vom Anwendungsfall ab. Enums besitzen ein paar nützliche Methoden, die es ermöglichen ein Enum anhand eines Strings zu bestimmen oder mit denen man über alle Werte iterieren kann. Dafür lassen sie sich nicht objektorientiert nutzen, da sie nicht durch Klassen, sondern durch eine Variable repräsentiert werden. Werden also Aufzählungen mit verschiedenen Verhaltensweisen benötigt empfiehlt es sich auf ADTs zu setzen.

Hinweis:
Wir müssen nicht unbedingt ein ‚case‘ vor ein ‚object‘ setzten wenn wir es innerhalb von Pattern Matchings benutzen wollen. Das ‚case‘ hat tatsächlich nur ein geringen Nutzen, da für ein ‚object‘ keine apply-, unapply- und all die anderen Methoden generiert werden müssen. Die einzige Methode, die uns durch das ‚case‘ geschenkt wird und die auch von Nutzen ist, ist toString(), die den Namen des ‚object‘ zurück gibt.

Syntaxbäume

Eine weitere Möglichkeit ADTs einzusetzen ergibt sich bei der Erstellung von Syntaxbäumen. Ein Syntaxbaum besteht aus lauter Expressions, die sich in Terme, Faktoren, Literale und andere Symbole gliedern lassen. Versuchen wir dies in Scala abzubilden:

sealed abstract class Exp
case class Lit(v: BigDecimal) extends Exp
case class Add(n1: Exp, n2: Exp) extends Exp
case class Sub(n1: Exp, n2: Exp) extends Exp
case class Mul(n1: Exp, n2: Exp) extends Exp
case class Div(n1: Exp, n2: Exp) extends Exp

def eval(n: Exp): BigDecimal = n match {
  case Lit(v) => v
  case Add(n1, n2) => eval(n1)+eval(n2)
  case Sub(n1, n2) => eval(n1)-eval(n2)
  case Mul(n1, n2) => eval(n1)*eval(n2)
  case Div(n1, n2) => eval(n1)/eval(n2)
}

Anhand der dargestellten Datentypen können wir bequem einen Syntaxbaum aufbauen und durch die eval-Methode evaluieren:

scala> val e1 = Mul(Add(Lit(3), Lit(4)), Sub(Lit(9), Lit(2)))
e1: Mul = Mul(Add(Lit(3),Lit(4)),Sub(Lit(9),Lit(2)))

scala> eval(e1)
res10: BigDecimal = 49

scala> (3+4)*(9-2)
res11: Int = 49

scala> val e2 = Div(Lit(7), Sub(Add(Lit(3), Lit(4)), Lit(3)))
e2: Div = Div(Lit(7),Sub(Add(Lit(3),Lit(4)),Lit(3)))

scala> eval(e2)
res12: BigDecimal = 1.75

scala> val e3 = Mul(Lit(BigDecimal("8643356779865464.5")), Lit(BigDecimal("78953642.159865456879")))
e3: Mul = Mul(Lit(8643356779865464.5),Lit(78953642.159865456879))

scala> eval(e3)
res14: BigDecimal = 682424498257544872878103.7104460553

Durch den Einsatz von BigDecimal als Datentyp können wir Zahlen beliebiger Genauigkeit nutzen. Möchten wir maximale Präzision beim errechnen der Zahlen, müssen wir BigDecimal mit einem String konstruieren. Bei der Eingabe von Int-Literalen müssen wir die apply-Methode von BigDecimal nicht explizit aufrufen. Die eval-Methode könnten wir dank Scalas Syntaxzucker auch so schreiben:

def eval(n: Exp): BigDecimal = n match {
  case Lit(v) => v
  case n1 Add n2 => eval(n1)+eval(n2)
  case n1 Sub n2 => eval(n1)-eval(n2)
  case n1 Mul n2 => eval(n1)*eval(n2)
  case n1 Div n2 => eval(n1)/eval(n2)
}

Das mag der ein oder andere ein wenig schöner finden als der normale Aufruf der Extraktoren. Ich möchte noch einmal daran erinnern, dass Scala objektorientiert ist, es steht uns also frei die eval-Methode direkt an den ADT zu heften:

sealed abstract class Exp {
  def eval: BigDecimal
}
case class Lit(v: BigDecimal) extends Exp {
  def eval = v
}
case class Add(n1: Exp, n2: Exp) extends Exp {
  def eval = n1.eval+n2.eval
}
case class Sub(n1: Exp, n2: Exp) extends Exp {
  def eval = n1.eval-n2.eval
}
case class Mul(n1: Exp, n2: Exp) extends Exp {
  def eval = n1.eval*n2.eval
}
case class Div(n1: Exp, n2: Exp) extends Exp {
  def eval = n1.eval/n2.eval
}

Wieder entfällt der Runtime-Overhead durch das Pattern Matching weil die Methoden polymorph aufgerufen werden können:

scala> Mul(Add(Lit(3), Lit(4)), Sub(Lit(9), Lit(2))).eval
res16: scala.math.BigDecimal = 49

scala> Div(Lit(7), Sub(Add(Lit(3), Lit(4)), Lit(3))).eval
res17: scala.math.BigDecimal = 1.75

Hörst du mich?

Das letzte Einsatzgebiet für Fallklassen, das ich euch zeigen möchte, ist die Objektkommunikation. Die einfachste Möglichkeit um Objekte miteinander kommunizieren zu lassen ist über dessen Methoden.

case class Person(name: String)

case class Computer(name: String) {
  
  private var isRunning = false
  
  def sayHello(p: Person) {
    if (isRunning)
      println("hello '%s'" format p.name)
  }
  
  def start() {
    println("starting up '%s'" format name)
    isRunning = true
  }
  
  def stop() {
    println("'%s' is now sleeping" format name)
    isRunning = false
  }
}

An diesem Code ist nichts Besonderes. Die Klasse Computer stellt verschiedene Methoden bereit über die man mit der Klasse kommunizieren kann. Das Vorgehen ist bekannt und funktioniert auch prächtig:

scala> val p = Person("lara")
p: Person = Person(lara)

scala> val c = Computer("wudiwutz")
c: Computer = Computer(wudiwutz)

scala> c.start()
starting up 'wudiwutz'

scala> c sayHello p
hello 'lara'

scala> c.stop()
'wudiwutz' is now sleeping

Komplizierter wird dieses Vorgehen dann, wenn mehrere Parameter an eine Methode übergeben werden sollen. Der Einfachheit halber würde man versuchen aus diesen Parametern ein Objekt zu bilden, das an die Methode übergeben werden kann. Dies hätte weiterhin den Vorteil, dass dem Objekt wiederum Verhalten mitgegeben werden kann. Durch Fallklassen besteht in Scala die Möglichkeit die Kommunikation über Objekte nicht nur zu wählen wenn einzelne Parameter unnötiger Aufwand wären. Schauen wir uns das an:

sealed abstract class Msg
case class Greeting(p: Person) extends Msg
case object Start extends Msg
case object Stop extends Msg

Für alle Methoden, die Computer bereitstellt, wurde ein dazugehöriges Objekt definiert. Start- und Stop-Signale soll es nur ein Mal geben, die Begrüßung soll beliebig oft erfolgen können – wir benutzen für sie also eine Klasse und kein object. Alle unsere Objekte sind zu einem ADT zusammengefasst, damit wir später auch nur mit diesem arbeiten müssen. Damit unser Computer nun mit den Nachrichten etwas anfangen kann müssen wir ihn ein wenig anpassen:

case class Computer(name: String) {
  
  private var isRunning = false
  
  def send(msg: Msg) = msg match {
    case Start => start()
    case Stop => stop()
    case Greeting(p) => sayHello(p)
  }
  
  private def sayHello(p: Person) {
    if (isRunning)
      println("hello '%s'" format p.name)
  }
  
  private def start() {
    println("starting up '%s'" format name)
    isRunning = true
  }
  
  private def stop() {
    println("'%s' is now sleeping" format name)
    isRunning = false
  }
}

Unsere bisherigen Methoden haben wir auf private gesetzt, damit sie von außen nicht mehr erreichbar sind. Sie sollen fortan nur noch zur Übersicht des Codes dienen. Die wichtigste Methode ist nun send, die eine Nachricht erwartet. Sie prüft welche Nachricht genau vorliegt und wählt dann die entsprechenden Aktionen aus.

scala> val p = Person("lara")
p: Person = Person(lara)

scala> val c = Computer("wudiwutz")
c: Computer = Computer(wudiwutz)

scala> c send Start
starting up 'wudiwutz'

scala> c send Greeting(p)
hello 'lara'

scala> c send Stop
'wudiwutz' is now sleeping

Wie unschwer zu erkennen hat sich die API unseres Objektes deutlich verkleinert. Das ganze System wirkt nun auch ein wenig objektorientierter, da Nachrichten nun nicht mehr nur Methoden, sondern ebenfalls Objekte sind. Dies ist mit ein klein wenig Laufzeit-Overhead verbunden, über den wir uns aber nicht groß zu kümmern brauchen, sofern wir keine zeitkritische Anwendungen schreiben müssen. Wollen wir, dass unser Computer auch antworten kann, benötigen wir weiter Objekte zum verschicken und eine Methode um diese zu empfangen:

case class ThrottleCpuPower(percent: Int) extends Msg

sealed abstract class Response extends Msg
case class CpuPower(power: Option[Int]) extends Response
case object NoResponse extends Response

Mit dem ADT Response können wir die Antworten des Computers spezialisieren um später nicht auf alle Nachrichten matchen zu müssen.

case class Person(name: String) {
  def reply(res: Response) = res match {
    case CpuPower(power) =>
      if (power.isDefined) println("new cpu power is %d%%" format power.get)
      else println("nothing changed")
    case NoResponse =>
  }
}

Unsere Personen-Klasse erhält eine Methode über die sie mit Nachrichten versorgt werden kann. Wir müssen auch unseren Computer anpassen, da dieser ja mit Nachrichten antworten soll:

// in class Computer
private var cpuPower = 80

def send(msg: Msg) = msg match {
  case Start => start()
  case Stop => stop()
  case Greeting(p) => sayHello(p)
  case _ =>
}

def sendAndResponse(msg: Msg) = msg match {
  case ThrottleCpuPower(percent) => owner reply CpuPower(throttle(percent))
  case _ => owner reply NoResponse
}

private def throttle(percent: Int) =
  if (cpuPower-percent < 20) None
  else {
    cpuPower -= percent
    Some(cpuPower)
  }

Es gibt eine neue öffentliche Methode namens sendAndResponse, an die alle Nachrichten geschickt werden, auf die geantwortet werden soll. In unserem Fall wollen wir nach einer Drosselung der CPU-Geschwindigkeit eine Rückmeldung mit der neuen Geschwindigkeit erhalten. Als Antwort wird ein Option gesendet. Damit haben wir die Möglichkeit das Signal über den Änderungszustand weiter anzupassen. Unser Computer soll bspw. immer erreichbar sein, was wir dadurch erreichen, dass eine Mindestmarke nie unterschritten werden darf. Ist dies der Fall soll nichts geändert werden. Die Antwort geht nun an einen mysteriösen owner, aber woher kommt dieser? Die einfachste Möglichkeit ist sicherlich ihn einfach über den Konstruktor zu injizieren:

case class Computer(name: String, owner: Person) {...}

Beachtet bitte, dass die send-Methode nun ein Default-Fall benötigt, andernfalls erhaltet ihr eine Warnung vom Compiler.

scala> val p = Person("lara")
p: Person = Person(lara)

scala> val c = Computer("wudiwutz", p)
c: Computer = Computer(wudiwutz,Person(lara))

scala> c send Start
starting up 'wudiwutz'

scala> c send Greeting(p)
hello 'lara'

scala> c sendAndResponse ThrottleCpuPower(30)
new cpu power is 50%

scala> c sendAndResponse ThrottleCpuPower(60)
nothing changed

scala> c send Stop
'wudiwutz' is now sleeping

Der Code funktioniert so wie erwartet. Wir erhalten unterschiedliche Ausgaben nachdem unser Computer eine Antwort verschickt hat.

Die Frage ist nun: Haben wir mit dieser Vorgehensweise im Vergleich zum kommunizieren mit Methoden etwas erreicht? In obigem Code halten sich die Vorteile in Grenzen. Unser System ist ein wenig objektorientierter, vielleicht auch ein wenig übersichtlicher. Das war es aber auch schon. Die wahren Stärken dieser Vorgehensweise können sich erst durch ein Framework entfalten, wie z.B. Akka:

case class Computer(name: String) extends Actor {
  def receive = {
    case Msg => self reply Response
  }
}

Akka ist ein Actor-Framework und ermöglicht eine parallele Abarbeitung unserer Nachrichten. Actoren sind nichts besonderes, man kann sie sich als Menschen vorstellen, die Nachrichten (z.B. gesprochenen Wörter) empfangen („hören“) und auch verschicken („sprechen“) können – und das alle auf einmal. Sie können dabei nicht wissen was für einen Zustand die Anderen gerade besitzen („Gedanken“) und es steht ihnen frei wie sie auf Nachrichten reagieren. Akka stellt so ein Actoren-Modell zur Verfügung und lässt uns damit komfortabel arbeiten. Wenn wir uns die receive-Methode angucken stellen wir fest, dass kein Default-Fall mehr angegeben ist und auch das match-Statement, das eine Nachricht matcht, ist nirgends zu erblicken. Weiterhin kann auf ein self-Objekt geantwortet werden, dessen Definition für uns verborgen bleibt. Wir haben hier schönen kurzen Code, der jedoch weitaus komplexer als unser obiges Beispiel ist. Dafür ist er aber auch einfacher zu benutzen und zu verstehen – sofern man die benutzten Konzepte verstanden hat.

Ich werde im Zuge dieses Tutorials näher auf Actoren und auch auf Akka eingeben und euch all das Wissen vermitteln, das ihr braucht um obigen Code ebenfalls korrekt anwenden zu können.

Lösungen: Extraktoren

  1. Wenn wir den Extraktor über eine Klasse erstellen können wir sein Verhalten anpassen.
    class Nth(n: Int) {
      def unapply(xs: Seq[Int]) = if (n < xs.size) Some(xs(n)) else None
    }
    val isSecond_? = new Nth(1)
    val isThird_? = new Nth(2)
    Vector(9, 13, 3, 73, 52) match {
      case isSecond_?(12) => println("second is twelve")
      case isThird_?(3) => println("third is three")
      case _ => println("nothing special found")
    }
    
  2. Der &-Extraktor ist sehr einfach. Es genügt, einfach einen Tuple2 mit dem Eingabestring zurückzugeben.
    object & {
      def unapply(a: String) = Some(a, a)
    }
    object StartsWith {
      def unapply(s: String) = s.headOption
    }
    object EndsWith {
      def unapply(s: String) = s.lastOption
    }
    
    "Hello World" match {
      case StartsWith('H') & EndsWith('d') => "yes"
      case _ => "no"
    }
    "Hello World" match {
      case StartsWith('g') | EndsWith('d') => "yes"
      case _ => "no"
    }
    
  3. Die Baumrepräsentation ähnelt der von IntList. Die Extraktoren sollten kein Problem mehr sein.
    
    abstract class IntTree
    
    class Node(val left: IntTree, val right: IntTree) extends IntTree
    object Node {
      def apply(left: IntTree, right: IntTree) = new Node(left, right)
      def unapply(n: Node) = Some(n.left, n.right)
    }
    
    class Leaf(val i: Int) extends IntTree
    object Leaf {
      def apply(i: Int) = new Leaf(i)
      def unapply(l: Leaf) = Some(l.i)
    }
    
    object Empty extends IntTree
    
    val tree: IntTree = Node(Leaf(8), Node(Node(Leaf(9), Empty), Node(Leaf(9), Empty)))
    tree match {
      case Node(Leaf(i @ 8), Node(Node(Leaf(j @ 9), Empty), _)) => "found:"+i+","+j
      case _ => "nothing found"
    }
    

Übungen: Extraktoren

  1. Erzeuge einen Extraktor, der es erlaubt mit Hilfe eines Indexes auf ein Element einer Seq zuzugreifen. Der Code
    Vector(8,2,5) match {
      case isSecond_?(2) => "yes"
      case _ => "no"
    }
    

    soll beispielsweise „yes“ ausgeben. Tipp: Benutze eine Klasse als Extraktor

  2. Die match-Expression erlaubt es mehrere Fälle zu „verodern“, sie aber nicht zu „verunden“. Folgendes ist möglich:
    "Hello World" match {
      case StartsWith('g') | EndsWith('d') => "yes"
      case _ => "no"
    }
    

    Dies hier aber nicht:

    "Hello World" match {
      case StartsWith('H') & EndsWith('d') => "yes"
      case _ => "no"
    }
    

    Finde einen Weg bei einer Eingabe von Strings, mehrere Extraktoren (hier: StartsWith und EndsWith) miteinander zu „verunden“. Tipp: & ist ebenfalls ein Extraktor.

  3. Schreibe Extraktoren für eine Baumrepräsentation:
    val tree: IntTree = Node(Leaf(8), Node(Node(Leaf(9), Empty), Node(Leaf(9), Empty)))
    tree match {
      case Node(Leaf(i @ 8), Node(Node(Leaf(j @ 9), Empty), _)) => "found:"+i+","+j
      case _ => "nothing found"
    }
    

    Entnehme die benötigten Klassen dem obigen Code.

Hier geht es zu den Lösungen.

Teil 12: Extraktoren

Wir haben in einem früheren Artikel bereits das Pattern Matching kennen gelernt. In dem dortigen Artikel musste ich euch für die Erklärungen, wie genau Pattern Matching nun funktioniert, auf einen späteren Zeitpunkt verweisen. In diesem Artikel werde ich euch die nötigen Erklärungen geben und nebenbei noch viele Beispiele bringen was wir mit Pattern Matching noch so alles machen können.

Einfache Extraktoren

Ein Beispiel, das ich schon gebracht habe, gab uns die Möglichkeit durch Pattern Matching einen Wert an Variablen zu binden:

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

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

Die Funktionsweise des obigen Codes obliegt keinesfalls nur den Fähigkeiten des Compilers daraus Variablenzuweisungen zu generieren. Wir können aktiv in diesen Prozess eingreifen und festlegen was wir haben wollen. Hierfür benötigen wir nur einen sogenannten Extraktor. Ein Extraktor ist syntaktischer Zucker des Compilers – wir können ihn durch eine unapply-Methode innerhalb eines object erstellen und dann auf ihn zugreifen:

object StringExtractor {
  def unapply(s: String): Option[String] = s(0) match {
    case 'u' => Some(s.substring(1).toUpperCase)
    case 'l' => Some(s.substring(1).toLowerCase)
    case _ => None
  }
}

Wir können den Extraktor bequem aufrufen indem wir in Klammern das zu extrahierende Objekt übergeben:

scala> val StringExtractor(s) = "uHello"
s: String = HELLO

Der Compiler sorgt dann dafür, dass die unapply-Methode aufgerufen wird. Auf welche Typen ein Extraktor angewendet werden kann hängt vom Typ des Parameters der unapply-Methode ab – in unserem Beispiel wäre es ein String. Der Typ unserer Variable, die durch den Extraktor erzeugt wird hängt vom Rückgabetyp der unapply-Methode ab. Zum besseren Verständnis hier der Code, den der Compiler aus dem Extraktor erzeugen würde:

scala> val s = StringExtractor.unapply("uHello") match {
     |   case Some(s) => s
     |   case None => throw new MatchError
     | }
s: String = HELLO

Versuchen wir unseren Extraktor mit einem anderen Typ zu füttern, bekommen wir vom Compiler direkt eine Fehlermeldung:

scala> val StringExtractor(s) = 5
<console>:13: error: scrutinee is incompatible with pattern type;
 found   : String
 required: Int
       val StringExtractor(s) = 5
                          ^

Der Extraktor selbst funktioniert sehr einfach. Er prüft ob der erste Buchstaben eines Strings ein u oder ein l ist und wenn ja wird der restliche Stringinhalt in lauter Groß- oder Kleinbuchstaben umgewandelt. Die unapply-Methode gibt dann das extrahierte Objekt in einem Option verpackt zurück. Option haben wir schon früher kennen gelernt. Es gibt unserem Compiler die Möglichkeit zu erkennen ob ein Extraktor erfolgreich war oder nicht. Geben wir ein Some mit Inhalt zurück, so war der Extrahiervoragng erfolgreich. Haben wir nichts zu extrahieren müssen wir ein None zurückgeben. Würden wir nur das extrahierte Objekt zurück geben hätte der Compiler keine Möglichkeit festzustellen ob der Extraktionsvorgang erfolgreich war. Das ist insofern problematisch, da nach einem fehlgeschlagenen Pattern direkt zum nächsten Pattern gesprungen wird. Wenn also die Möglichkeit besteht, dass unser Extraktor fehlschlagen kann, dann müssen wir einen Default-Fall festlegen:

def extract(s: String) = s match {
  case StringExtractor(s) => s
  case _ => s
}

scala> extract("lHello World")
res4: String = hello world

scala> extract("uHello World")
res5: String = HELLO WORLD

scala> extract("hello")
res6: String = hello

Hätten wir keinen Default-Fall würde unser Code zur Laufzeit einen MatchError werfen:

scala> val StringExtractor(s) = "hello"
scala.MatchError: hello (of class java.lang.String)
<stack trace>

Anmerkung:
Der Extraktor muss mit einem Großbuchstaben anfangen, bei einem Kleinbuchstaben würde er nicht erkannt werden. Warum das so ist hab ich im Artikel über Pattern Matching bereits geschrieben.

Der Compiler hat hier keine Möglichkeit zu überprüfen ob der Code zur Laufzeit auch funktionieren wird. Er könnte zwar herausfinden was der Extraktor genau macht, er kann aber nicht wissen was für Eingabewerte er erhält. In obigem Beispiel haben wir einen statisch festgelegten String, aber was wenn der String durch eine Benutzereingabe erzeugt wird oder aus einer Datenbank kommt? Um zu vermeiden, dass unser Code hier einen potenziellen Fehler erzeugt, müssen wir unseren Extraktor ändern:

object StringExtractor {
  def unapply(s: String) = s(0) match {
    case 'u' => Some(s.substring(1).toUpperCase)
    case 'l' => Some(s.substring(1).toLowerCase)
    case _ => Some(s)
  }
}

Wir haben den Rückgabewert der unapply-Methode geändert. Anstatt ein Option erhalten wir nun nur noch ein Some. Dadurch funktioniert unser Code immer und wir können uns die Hilfsmethode zum Extrahieren ersparen:

scala> val StringExtractor(s) = "hello"
s: java.lang.String = hello

scala> val StringExtractor(s) = "uhello"
s: java.lang.String = HELLO

Mehrfache Extraktoren

Unser Code funktioniert jetzt für einen einzigen Parameter, wenn wir uns aber an das Tuple-Beispiel zurück erinnern, dann hatten wir dort aber mehrere Parameter:

scala> val (a, b) = (5, "hello")
a: Int = 5
b: java.lang.String = hello

Es ist gar nicht schwer dieses Verhalten selbst nachzubauen, wir müssen nur die Signatur der unapply-Methode ändern:

class Pair(val a: Int, val b: Int)
object Pair {
  def unapply(p: Pair): Option[(Int, Int)] = Some((p.a, p.b))
}

scala> val Pair(a, b) = new Pair(8, 12)
a: Int = 8
b: Int = 12

Die unapply-Methode erwartet jetzt ein Pair-Objekt. Zurückgeben tut sie dann alle zu extrahierenden Parameter in Tuple-Form (wieder gepackt in ein Option). Unsere Pair-Klasse können wir hier leider nur mit zwei Ints aufrufen. Wollen wir sie lieber mit einem String aufrufen wie beim Tuple Beispiel gezeigt, müssten wir den Parametertyp ändern, was jedoch sehr umständlich ist. In einem späteren Kapitel über parametrisierte Typen werde ich euch aber zeigen wie man dieses Problem geschickt lösen kann.

Anmerkung:
Wir können beim Erzeugen eines Tuples die runden Klammern weglassen wenn die Parameter schon in runden Klammern stehen. Das Codestück

Some((p.a, p.b))

können wir also auch

Some(p.a, p.b)

schreiben. Ihr glaubt es nicht? Dann probiert es aus!

Im Artikel über Pattern Matching habe ich euch versprochen folgenden Code zum Laufen zu bekommen:

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

Die Implementierungen sind wieder nicht besonders schwer:

class Person(val name: String, val age: Int, val address: Address)
object Person {
  def unapply(p: Person) = Some(p.name, p.age, p.address)
}
class Address(val city: String, val zip: Int, val street: String)
object Address {
  def unapply(a: Address) = Some(a.city, a.zip, a.street)
}

Das Extrahieren beschränkt sich dann auf bereits bekannte Sachen:

scala> val p = new Person("helen", 31, new Address("musterstadt", 12345, "musterstraße"))
p: Person = Person@22f49424

scala> val Person(name, age, address @ Address(city, zip, street)) = p
name: String = helen
age: Int = 31
address: Address = Address@68e2cd6f
city: String = musterstadt
zip: Int = 12345
street: String = musterstraße

Mit Hilfe des @-Zeichen können wir gleichzeitig die Adresse extrahieren aber auch deren Instanz an eine Variable binden.

Unsere Extraktor müssen wir übrigens nicht zwingend an ein object binden, wir können sie auch durch Klassen erzeugen:

class StringExtractor(u: Char = 'u', l: Char = 'l') {
  def unapply(s: String) = s(0) match {
    case `u` => Some(s.substring(1).toUpperCase)
    case `l` => Some(s.substring(1).toLowerCase)
    case _ => Some(s)
  }
}

Dies erlaubt uns unsere Extraktoren ein wenig anzupassen. Wir können bei der Objekterzeugung nämlich angeben auf welche Buchstaben der Extraktor prüfen soll.

scala> val SE1 = new StringExtractor()
SE1: StringExtractor = StringExtractor@7137f424

scala> val SE1(s) = "uhello"
s: java.lang.String = HELLO

scala> val SE2 = new StringExtractor('a', 'b')
SE2: StringExtractor = StringExtractor@1752b8b

scala> val SE2(s) = "ahello"
s: java.lang.String = HELLO

scala> val SE2(s) = "uhello"
s: java.lang.String = uhello

Beachtet hier bitte wieder dass, die Extraktoren innerhalb einer match-Expression groß geschrieben sein müssen und dass die Variablen mit Backticks referenziert werden müssen. Die mit val definierten Extraktoren dürfen wir auch kein schreiben.

Sequenzielle Extraktoren

Wir wissen jetzt wie wir einen Extraktor mit einem Parameter und auch mit mehreren Parametern erstellen, aber was ist wenn wir die Anzahl der Parameter gar nicht wissen? Das ist bspw. dann der Fall wenn wir eine Liste haben:

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

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

Die Liste kann eine beliebige Anzahl an Elementen besitzen, unsere unapply-Methode müsste also ein Option mit einer Seq zurückgeben. Schreiben wir den dazugehörigen Code:

class Container(val x: Int*)
object Container {
  def unapply(p: Container): Option[Seq[Int]] = Some(p.x)
}

Varargs sind in Scala nichts anderes als eine Seq, wir können sie also direkt zurückgeben. Testen wir den Code gleich noch:

scala> val Container(x1, x2, x3) = new Container(45, 32, 107)
<console>:9: error: wrong number of arguments for object Container
       val Container(x1, x2, x3) = new Container(45, 32, 107)
                    ^
<console>:9: error: recursive value x$1 needs type
       val Container(x1, x2, x3) = new Container(45, 32, 107)
                     ^

scala> val Container(x) = new Container(45, 32, 107)
x: Seq[Int] = WrappedArray(45, 32, 107)

Hm, das ist aber nicht das was wir erwartet haben. Aber es ist das spezifizierte Verhalten. Was haben wir denn genau hingeschrieben? Unsere unapply-Methode gibt eine Seq zurück. So weit so gut, aber woher soll der Compiler wissen ob wir durch das Pattern Matching eine Seq oder aber die Elemente der Seq erhalten wollen? Er kann es nicht wissen, deshalb können wir auch nur auf eine Seq matchen und nicht auf einzelne Elemente. Und da wir nur ein Element zurückgeben – nämlich die Seq – erhalten wir auch eine Fehlermeldung wenn wir versuchen den Extraktor mit mehreren Parametern aufzurufen. Bei List funktioniert es aber doch auch. Was ist dort anders? Tatsächlich kennt der Scala Compiler nicht nur eine unapply-Methode, sondern deren zwei. Die zweite nennt sich unapplySeqund kann als Ersatz zur normalen unapply-Methode genutzt werden. Wie der Name schon suggeriert ermöglicht sie uns nicht nur eine Seq zurückzugeben, sondern auch deren Elemente.

class Container(val x: Int*)
object Container {
  def unapplySeq(p: Container): Option[Seq[Int]] = Some(p.x)
}

scala> val Container(x) = new Container(45, 32, 107)
scala.MatchError: Container@16b5d4e1 (of class Container)
<stack trace>

scala> val Container(x @ _*) = new Container(45, 32, 107)
x: Seq[Int] = WrappedArray(45, 32, 107)

scala> val Container(head, tail @ _*) = new Container(45, 32, 107)
head: Int = 45
tail: Seq[Int] = WrappedArray(32, 107)

scala> val Container(x1, x2, x3) = new Container(45, 32, 107)
x1: Int = 45
x2: Int = 32
x3: Int = 107

Toll, nicht wahr? Es funktioniert alles so wie erwartet. Die unapplySeq-Methode unterliegt nur einer kleinen Einschränkung: Wir dürfen sie nicht zusammen mit einer unapply-Methode bereitstellen. Sollten beide Methoden existieren, so wird der Compiler nur die unapply-Methode auswählen und die andere nicht weiter beachten.

Jetzt haben wir für den Schluss aber noch etwas, das gerne besprochen werden möchte:

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

Diese Schreibweise unterscheidet sich von den Vorherigen. Wir müssen hier nicht mehr umständlich unseren Extraktor definieren. Stattdessen sieht es eher so aus wie wenn wir auf die Prepend-Methode von List zurückgreifen würden:

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

Der einzige Unterschied ist, dass das Nil am Schluss fehlt. Aber warum fehlt es? Die Antwort darauf wird ein wenig klarerer wenn wir unseren Code ein wenig umändern:

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

Huch, was war das? Es war mal wieder syntaktischer Zucker des Compilers, der uns hier das Leben erleichtert. Immer dann wenn eine Klasse zwei Typparameter erwartet (die wir später im Detail kennen lernen werden) oder einen Konstruktor mit zwei Parametern besitzt, besteht die Möglichkeit, dass wir sie nicht in der Form

Class(obj1, obj2)

sondern als

obj1 Class obj2

aufrufen können. Das Gleiche haben wir oben beim extrahieren der List-Elemente gemacht. Diese Schreibweise wird uns aber nur bei Typparameter und Konstruktoren erlaubt und sonst nirgends. Aber wieso funktioniert unser Code dann? Das Symbol :: ist doch eine Methode in List? Ja, es ist eine Methode aber auch der Aufruf eines Extraktors. Genau genommen existiert :: zwei Mal – einmal als Methode und einmal als Klasse. Die Klasse :: stellt einen geeigneten Konstruktor bereit, der uns diese Schreibweise erlaubt. Hier die Klassendefinition von scala.collection.immutable.:::

final case class ::[B](
  private var hd: B,
  private[scala] var tl: List[B])
extends List[B] {...}

Die Klassendefinition ist für den Anfang ein wenig verwirrend und genau deshalb werden wir uns jetzt eine eigene List schreiben. Das hilft uns nicht nur zu verstehen wann genau die Methode :: und wann das Objekt :: aufgerufen wird – es hilft uns vor allem auch die Stärken und Schwächen von List kennen zu lernen. Fangen wir damit also gleich an. Und danach gibt es noch ein paar Übungsaufgaben bei denen ihr testen könnt ob ihr auch alles verstanden habt und ohne meine Hilfe zurechtkommt.

Praxisbeispiel: Implementierung von List

Eine List ist eine einfach verkette Liste. Jedes Stück der List besitzt neben dem Element, das es aufnimmt noch eine Referenz auf das nächste Stück der Liste. Daraus folgt ein einfacher Konstruktor:

class IntList(val head: Int, val tail: IntList)
object IntList {
  def apply(head: Int, tail: IntList) = new IntList(head, tail)
}

Wir beschränken unsere Listimplementierung darauf, dass sie nur Ints aufnehmen kann, dann müssen wir uns noch nicht mit parametrisierten Typen herumschlagen. Durch die Implementierung einer apply-Methode können wir uns fortan gleich noch das new sparen. Wir können nun schon eine List erstellen:

scala> val xs = IntList(1, IntList(2, IntList(3, null)))
xs: IntList = IntList@7137f424

Das null ist uns jetzt noch ein Dorn im Auge. Es ist nicht typsicher und kann zu NullPointerExceptions führen. Versuchen wir es also zu umgehen:

class IntNil extends IntList(0, null)
object IntNil {
  def apply(): IntList = new IntNil
}

scala> val xs = IntList(1, IntList(2, IntList(3, IntNil())))
xs: IntList = IntList@57a68215

Aber wirklich besser ist das auch nicht. Wir haben das null jetzt nur vom Anwendungscode in die Bibliothek verlagert. Außerdem erzeugen wir bei jedem Aufruf von IntNil ein neues Objekt. Daraus folgt:

scala> IntNil() != IntNil()
res4: Boolean = true

Anmerkung:
Achtet darauf, dass ihr IntNil() aufruft und nicht nur IntNil. Wenn ihr die Klammern weg lässt referenziert ihr nicht die apply-Methode sondern den Typ IntNil. Dessen genaue Bedeutung kann uns im Moment egal sein, es muss nur klar sein, dass er existiert.

Wir dürfen also nur eine Instanz von IntNil besitzen. Wir erreichen das am besten wenn wir unseren Code ein wenig umbauen:

abstract class IntList {
  def head: Int
  def tail: IntList
}

class Cons(val head: Int, val tail: IntList) extends IntList
object Cons {
  def apply(head: Int, tail: IntList) = new Cons(head, tail)
}

object IntNil extends IntList {
  def head = throw new UnsupportedOperationException("nil head")
  def tail = throw new UnsupportedOperationException("nil tail")
}

Anstatt Verhalten durch IntNil auszutauschen haben wir nun eine polymorphe Datenstruktur, deren genaues Verhalten von den Subklassen abhängen. Scala erlaubt uns das Überschreiben von Methoden durch Attribute (so geschehen in Cons), da der Compiler für die Attribute entsprechende Zugriffsmethoden erzeugt.
Das null ist einer Exception gewichen, welche direkt durch die Methoden head und tail zurückgegeben wird. Die Codeerzeugung unterscheidet sich nicht groß von der vorherigen Version:

scala> val xs = Cons(1, Cons(2, Cons(3, IntNil)))
xs: Cons = Cons@4f86f5f

Ergänzen wir unseren Code durch eine vernünftige String-Repräsentation:

abstract class IntList {
  def head: Int
  def tail: IntList
  def isEmpty: Boolean

  override final def toString = {
    val sb = StringBuilder.newBuilder
    sb append "IntList("
    sb append head

    var xs = tail
    while (!xs.isEmpty) {
      sb append ", "
      sb append xs.head
      xs = xs.tail
    }

    sb append ")"
    sb.toString
  }
}

class Cons(val head: Int, val tail: IntList) extends IntList {
  def isEmpty = false
}
object Cons {
  def apply(head: Int, tail: IntList) = new Cons(head, tail)
}

object IntNil extends IntList {
  def head = throw new UnsupportedOperationException("nil head")
  def tail = throw new UnsupportedOperationException("nil tail")
  def isEmpty = true
}

Die isEpmty-Methode spart uns einen Vergleich auf IntNil, welchen man jetzt aber durchaus machen könnte, da es nur eine Instanz davon gibt. Die Ausgabe ist gleich zufriedenstellender.

scala> val xs = Cons(1, Cons(2, Cons(3, IntNil)))
xs: Cons = IntList(1, 2, 3)

Die Erzeugung der List sieht noch nicht besonders elegant aus. Ändern wir das:

// in IntList
def :: (i: Int) = new Cons(i, this)

scala> val xs = 1 :: 2 :: 3 :: IntNil
xs: Cons = IntList(1, 2, 3)

Als nächstes wollen wir auf die tolle Konkatenationsschreibweise zurückgreifen:

// in object Cons
def unapply(c: Cons) = Some(c.head, c.tail)

scala> val x1 Cons (x2 Cons x3) = 1 :: 2 :: 3 :: IntNil
x1: Int = 1
x2: Int = 2
x3: IntList = IntList(3)

Die Klammern werden leider benötigt da der Compiler sonst durch die Auswertungsreihenfolge (von links nach rechts) durcheinander kommt. Wir können das ändern indem wir der Cons-Klasse einen Namen geben, der mit einem Doppelpunkt endet.

class :: (val head: Int, val tail: IntList) extends IntList {
  def isEmpty = false
}
object :: {
  def apply(head: Int, tail: IntList) = new ::(head, tail)
  def unapply(c: ::) = Some(c.head, c.tail)
}

Wenn wir alle Vorkommen von Cons durch :: ersetzen, dann können wir durch die umgekehrte Auswertungsreihenfolge die Klammern weglassen:

scala> val head :: tail = 1 :: 2 :: 3 :: IntNil
head: Int = 1
tail: IntList = IntList(2, 3)

scala> val x1 :: x2 :: x3 :: IntNil = 1 :: 2 :: 3 :: IntNil
x1: Int = 1
x2: Int = 2
x3: Int = 3

Das war sie schon. Die ganze „Magie“ der Extraktoren. Zu Schluss noch eine Extraktor-Implementierung für unsere IntList:

object IntList {
  def apply(a: Int*) = {
    def loop(xs: Seq[Int], ys: IntList): IntList =
      if (xs.isEmpty) ys else loop(xs.tail, xs.head :: ys)
    loop(a, IntNil).reverse
  }

  def unapplySeq(a: IntList) = {
    def loop(xs: IntList, ys: List[Int]): List[Int] =
      if (xs.isEmpty) ys else loop(xs.tail, xs.head :: ys)
    Some(loop(a, Nil).reverse)
  }
}

// in IntList
def reverse: IntList = {
  def loop(xs: IntList, ys: IntList): IntList =
    if (xs.isEmpty) ys else loop(xs.tail, xs.head :: ys)
  loop(this, IntNil)
}

Der Code ist ein wenig umständlich. Wir müssen zuerst die Listen aufbauen und sie dann umdrehen. Wir haben leider keine Möglichkeit eine Liste direkt rückwärts aufzubauen, da sie nur einfach verkettet ist. Aber zumindest funktioniert der Code:

scala> val xs = IntList(1,2,3)
xs: IntList = IntList(1, 2, 3)

scala> val xs = IntList(1,2,3)
xs: IntList = IntList(1, 2, 3)

scala> val IntList(head, tail @ _*) = IntList(1, 2, 3)
head: Int = 1
tail: Seq[Int] = List(2, 3)

scala> val IntList(head, tail @ _*) = IntList(1 to 3: _*)
head: Int = 1
tail: Seq[Int] = List(2, 3)

Ihr erinnert euch doch hoffentlich noch an Ranges, die man im dritten Beispiel bewundern kann.

Die apply-Methode könnte man übrigens auch so schreiben:

// in object IntList
def apply(a: Int*) = (a :\ (IntNil: IntList)) { _ :: _ }

Das wäre die funktionale Herangehensweise an die Erzeugung einer geeigneten Liste. Das will ich aber nicht erklären, sonder mal nur so in den Raum werfen, damit ihr wisst was euch erwartet wenn ihr mir treu bleibt und fleißig weiter lest. 😉

Zum Abschluss noch die komplette Implementierung von IntList:

object IntList {
  def apply(a: Int*) = {
    def loop(xs: Seq[Int], ys: IntList): IntList =
      if (xs.isEmpty) ys else loop(xs.tail, xs.head :: ys)
    loop(a, IntNil).reverse
  }

  def unapplySeq(a: IntList) = {
    def loop(xs: IntList, ys: List[Int]): List[Int] =
      if (xs.isEmpty) ys else loop(xs.tail, xs.head :: ys)
    Some(loop(a, Nil).reverse)
  }
}

abstract class IntList {
  def head: Int
  def tail: IntList
  def isEmpty: Boolean

  def :: (i: Int) = new ::(i, this)

  def reverse: IntList = {
    def loop(xs: IntList, ys: IntList): IntList =
      if (xs.isEmpty) ys else loop(xs.tail, xs.head :: ys)
    loop(this, IntNil)
  }

  override final def toString = {
    val sb = StringBuilder.newBuilder
    sb append "IntList("
    sb append head

    var xs = tail
    while (!xs.isEmpty) {
      sb append ", "
      sb append xs.head
      xs = xs.tail
    }

    sb append ")"
    sb.toString
  }
}

class :: (val head: Int, val tail: IntList) extends IntList {
  def isEmpty = false
}
object :: {
  def apply(head: Int, tail: IntList) = new ::(head, tail)
  def unapply(c: ::) = Some(c.head, c.tail)
}

object IntNil extends IntList {
  def head = throw new UnsupportedOperationException("nil head")
  def tail = throw new UnsupportedOperationException("nil tail")
  def isEmpty = true
}

Übungen: Pattern Matching

Für den geneigten Leser hier einige Übungen zu Pattern Matching. Versucht alle Aufgaben möglichst mit dem „functional way“ zu lösen, d.h. verwendet unveränderliche Objekte. Schaut, dass ihr also anstelle von Schleifen die gute alte Rekursion benutzt, wenn möglich sogar Tail-Rekursion. U.u. könnt ihr auch mit der for-Expression auf eine Lösung kommen. Für die meisten Aufgaben gibt es eine entsprechende Methode in den Collections, versucht aber möglichst ohne diese auszukommen (ausgenommen sind davon die Methoden um Objekte zu einer Liste hinzuzufügen). Ihr könnt mit Hilfe der vorgefertigten Methoden aber eure eigene Lösungen vergleichen.

  1. Ermittle aus einer List[Int] die Anzahl der Elemente. (xs.size)
  2. Ermittle aus einer List[Int] die Summe aller Elemente. (xs.sum)
  3. Spiegle eine List[Int] so, dass ein Palindrom entsteht. Eine List(1, 2, 3) soll also List(1, 2, 3, 3, 2, 1) ergeben.
  4. Füge zwischen jedes Element aus einer List[String] einen weiteren String ein, was z.B. ein Trennzungszeichen sein kann. Eine List(„foo“, „bar“, „hello“, „world“) soll mit den Trennungszeichen “ : “ den String „foo : bar : hello : world“ ergeben. Beachte, dass keine Trennungszeichen am Anfang und zum Schluss kommen (xs mkString “ : „).
  5. Ebne verschachtelte Listen zu einer einzigen Liste. Aus dem Konstrukt „List(List(1, 2, 3), 4, 5, List(6), Nil, List(7, List(8, 9)))“ soll „List(1, 2, 3, 4, 5, 6, 7, 8, 9)“ entstehen (xs.flatten).
  6. Vervielfältige ein Element aus einer Liste eine bestimmte Anzahl mal. Aus „List(1, 5, 5, 3, 9)“ soll mit dem Multiplikator 3 die Liste „List(1, 1, 1, 5, 5, 5, 5, 5, 5, 3, 3, 3, 9, 9, 9)“ entstehen.
  7. Gruppiere die Elemente einer Liste zu Unterlisten. Eine Liste mit den Elementen 1 bis 14 und der Gruppengröße 4 soll  „List(List(1, 2, 3, 4), List(5, 6, 7, 8), List(9, 10, 11, 12), List(13, 14))“ ergeben. Falls die Elemente nicht aufgehen, werden sie einfach so zum Schluss hinzugefügt (xs grouped n).
  8. Lösche alle Duplikate aus einer Liste. Schaue, dass dabei die Reihenfolge der Element nicht vertauscht wird. Eine Liste mit den Elementen „1, 1, 1, 2, 4, 7, 2, 8, 3, 6, 2“ soll „List(1, 2, 4, 7, 8, 3, 6)“ ergeben.

Hier geht es zu den Lösungen.

Lösungen: Pattern Matching

  1. Die wohl einfachste Lösung:
    def size(xs: List[Int]): Int =
      if (xs.isEmpty) 0 else 1+size(xs.tail)
    

    Mit Pattern Matching:

    def size(xs: List[Int]): Int = xs match {
      case Nil => 0
      case _ :: tail => 1+size(tail)
    }
    

    Bei diesen Lösungen kann es aber zu einem StackOverflowError kommen wenn die Listen nur lang genug sind. Deshalb sollte die Tail-rekursive Lösung bevorzugt werden:

    def size(xs: List[Int], acc: Int = 0): Int = xs match {
      case Nil => acc
      case _ :: tail => size(tail, acc+1)
    }
    
  2. Gleiche Lösungsstrategie wie bei der vorherigen Aufgabe:
    def sum(xs: List[Int], acc: Int = 0): Int = xs match {
      case Nil => acc
      case head :: tail => sum(tail, acc+head)
    }
    
  3. Wenn wir die Liste umdrehen und dann an das Original hängen haben wir es einfach:
    def toPalindrome(xs: List[Int]): List[Int] = {
      def reverse(xs: List[Int], ys: List[Int]): List[Int] =
        if (xs.isEmpty) ys else reverse(xs.tail, xs.head :: ys)
      xs ++ reverse(xs, Nil)
    }
    
  4. Eine Lösung mit einer lokalen Methode um über die Liste zu iterieren:
    def intersperse(sep: String, xs: List[String]): String = {
      def loop(xs: List[String], s: String): String = xs match {
        case Nil => s
        case head :: Nil => s+head
        case head :: tail => loop(tail, s+head+sep)
      }
      loop(xs, "")
    }
    
  5. Wir benötigen eine List[Any], da wir durch die Verschachtelungen nicht wissen können was für Elemente in der Liste vorliegen.
    def flatten(xs: List[Any]): List[Any] = xs match {
      case Nil => Nil
      case (head: List[_]) :: tail => flatten(head) ::: flatten(tail)
      case  head :: tail => head :: flatten(tail)
    }
    
  6. Hier müssen wir uns sogar zweier lokalen Methoden bedienen. Eine, die über die Ausgangsliste iteriert und eine Andere, die die Elemente vervielfältigt.
    def multiplicateElems(n: Int, xs: List[Int]) = {
      def mul(i: Int, elem: Int, xs: List[Int]): List[Int] =
        if (i == 0) xs else mul(i-1, elem, elem :: xs)
      def loop(xs: List[Int], ys: List[Int]): List[Int] =
        if (xs.isEmpty) ys else loop(xs.tail, mul(n, xs.head, Nil) ::: ys)
      loop(xs, Nil).reverse
    }
    
  7. Wir müssen hier aufpassen, dass wir alle gruppierten Elemente wieder umdrehen, da die Listen falsch herum aufgebaut werden.
    def group(n: Int, xs: List[Int]): List[List[Int]] = {
      def loop(i: Int, xs: List[Int], ys: List[Int], zs: List[List[Int]]): List[List[Int]] =
        if (xs.isEmpty) ys.reverse :: zs
        else if (i == 0) loop(n, xs, Nil, ys.reverse :: zs)
        else loop(i-1, xs.tail, xs.head :: ys, zs)
      loop(n, xs, Nil, Nil).reverse
    }
    
  8. Das ist nicht schwer. Wir müssen nur schauen ob ein Element in der neuen Liste schon vorhanden ist.
    def compress(xs: List[Int]): List[Int] = {
      def loop(xs: List[Int], ys: List[Int]): List[Int] =
        if (xs.isEmpty) ys
        else if (ys contains xs.head) loop(xs.tail, ys)
        else loop(xs.tail, xs.head :: ys)
      loop(xs, Nil).reverse
    }
    

Noch eine Anmerkung zum Schluss: Das ständige Umdrehen der Liste mag nicht besonders schön sein, aber es ist die einfachste Möglichkeit mit der wir unsere Listen korrekt aufbauen können. Die List-Implementierung von Scala benutzt intern ein veränderliches letzten Element, das es erlaubt neue Elemente effizient ans Ende zu hängen.

Da wir ja aber das funktionale Programmieren lernen wollen, bestehen wir lieber auf eine unveränderliche Liste und drehen sie am Schluss einfach um.

Teil 11: Vererbung

Wir verfügen bereits über die Grundkenntnisse in Scalas Objektorientierung, gehen wir also noch ein wenig tiefer hinein in den Kaninchenbau.

Schauen wir uns noch einmal eine der einfachsten Klassen an:

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

Der Compiler generiert uns für die beiden Attribute name und age Getter und Setter. Das ist praktisch, was machen wir aber wenn wir in den Gettern oder Settern noch gerne etwas anderes erledigen wollen? Vielleicht müssen die an den Setter übergebenen Werte erst auf Gültigkeit geprüft werden? Oder aber wir wollen alle Zugriffe auf ein Attribut mitloggen.

Für diese Anwendungsfälle dürfen wir dem Compiler nicht die vollständige Codegenerierung überlassen, wir müssen selbst Hand anlegen. Sobald wir das val vor dem Attributnamen weglassen generiert uns der Compiler keine Access-Methoden mehr – wir können sie nun von Hand erstellen.

class Person(nameOfBirth: String, val age: Int = 0) {
  def name: String = {
    println("someone wants to know information of: "+nameOfBirth)
    nameOfBirth
  }
}

Da sowohl die Namen der Methoden und der Variablen vom Compiler innerhalb des gleichen Sichtbarkeitsbereichs verwaltet werden, können wir nicht die gleichen Namen vergeben. Wir müssen einen der beiden Namen ändern, in diesem Fall hat es den Konstruktorparameter getroffen. Das Ändern der öffentlichen Felder sollte man wenn möglich vermeiden, da es u.U. noch anderen Code gibt, der auf die Member unserer Klasse zugreift. Würden wir die Schnittstelle zu unserer Klasse ändern, müsste auch aller Code geändert werden, der auf die Schnittstelle zugreift. Hier kommt auch das als „Uniform Access Principle“ genannte Prinzip zum tragen, das besagt, dass die öffentlichen Member einer Schnittstelle mit möglichst der gleichen Notation angesprochen werden können. Für unsere Klasse heißt das, dass es egal ist ob unser Feld im Konstruktor oder innerhalb des Klassenkörpers deklariert wurde. Wir können immer mit dem Identifier „name“ auf das Feld zugreifen wobei der Initialisierungswert der Variable nach außen hin unsichtbar bleibt. Hier findet sich ein ganz brauchbarer Artikel zu diesem Thema.

scala> val p = new Person("max")
p: Person = Person@41cc5b64

scala> p.name
someone wants to know information of: max
res4: String = max

scala> p.nameOfBirth
:10: error: value nameOfBirth is not a member of Person
              p.nameOfBirth
                ^

Getter sind sehr intuitiv zu erstellen, bei Settern sieht das etwas anders aus:

class Person(val name: String, private var initAge: Int) {
  def age = initAge
  def age_=(age: Int) {
    if (age >= 0)
      initAge = age
  }
}

Das Problem hier ist, dass wir nicht einfach so das Attribut als veränderlich kennzeichnen können, da wir keine Methode gleichen Namens erstellen können mit der wir auf die Variable zugreifen können. Wir benötigen also eine interne Variable, die den Zustand speichert und auch einen Getter, wenn wir wollen, dass man auf die Variable Zugriff erhält. Die Syntax für den Methodenkopf eines Setters lautet:

def <def_name>_=(<param>)

Nach dem Namen der Methode folgt ein Unterstrich und ein Gleichheitszeichen bevor dann der Parameter notiert werden kann. Sieht komisch aus? Ja, aber ist es auch komisch in der Handhabung?

scala> val p = new Person("markus", 45)
p: Person = Person@5d17bf94

scala> p.age_=(46)

scala> p.age
res10: Int = 46

scala> p.age = 47
p.age: Int = 47

scala> p.age
res11: Int = 47

Der erste Methodenaufruf sieht so aus wie wir es erwartet haben. Beim zweiten hingegen begegnen wir wieder ein wenig syntaktischem Zucker. Der Compiler erlaubt uns den Unterstrich und die Klammern wegzulassen. Der Grund ist wieder der Uniform Access Modifier: Von außen soll nicht erkenntlich sein ob wir auf eine Methode oder auf eine Variable zugreifen.

Anmerkung:
Die Schreibweise

def <def_name>_<operator>(<param>)

erlaubt uns Alphabets- und Sonderzeichen zusammen in einem Identifier zu benutzen.
Wir können also Namen wie hello_+-* oder isset_? erstellen. Besonders letztere Schreibweise findet man des Öfteren, da es nochmal verdeutlichen kann, dass ein Boolean zurückgeben wird. Im Gegensatz zum Setter erlaubt uns der Compiler aber nicht den Unterstrich beim Aufruf des Identifiers wegzulassen. Von diesem Syntaxzucker kann man ausschließlich beim Setter Gebrauch machen. Die Kombination der Zeichen ist auch nur erlaubt wenn die Sonderzeichen zum Schluss kommen. Namen wie +_hello oder !_x_! sind ungültig.

Abstrakte Member

Manchmal haben wir Objekte, die im Grunde zusammengehören, da sie fast gleich funktionieren und sich nur in wenigen Punkten unterscheiden. Damit wir jetzt nicht bei allen solch zusammengehörenden Objekten fast den gleichen Code schreiben müssen besteht die Möglichkeit, das Verhalten, das für alle Objekte identisch ist, nur in einem Objekt zu definieren und es dann an andere Objekte zu vererben. Das Objekt, das Verhalten an ein anderes weitergibt wird Ober- oder Vaterobjekt genannt – bei der Klasse in der das Verhalten definiert wurde spricht man neben Oberklasse auch von Superklasse.

abstract class Person {
  def sayHello() { println("hello") }
  def doWork()
}
class Manager extends Person {
  def doWork() { println("rake in money") }
}
class Programmer extends Person {
  def doWork() { println("write code") }
}

Unsere Superklasse wäre hier Person, die zwei Methoden besitzt, eine mit einem Verhalten und eine ohne eins. Mit dem Schlüsselwort extends erstellen wir eine Vererbungshierarchie und weisen den Klassen Manager und Programmer das Verhalten von Person zu. Ins Auge dürfte uns gleich noch das abstract fallen. Was bedeutet es? Deklarieren wir eine Klasse als abstrakt heißt das, dass wir nicht wollen, dass irgendjemand davon irgendwann mal ein Objekt erstellt. Falls es dennoch mal jemand versuchen sollte bekommt er eine Fehlermeldung:

scala> new Person
:9: error: class Person is abstract; cannot be instantiated
              new Person
              ^

Dies schützt uns davor, dass ein Objekt erstellt wird, dessen Verhaltensweisen noch gar nicht festgelegt wurden, was bei Person mit der Methode doWork genau der Fall ist. Die Methode ist ebenfalls abstrakt, wir müssen dies aber nicht extra angeben. Es reicht einfach den Methodenkörper wegzulassen – der Compiler erkennt dann selbstständig, dass die Methode noch eine Implementierung benötigt. Wenn eine Klasse abstrakte Member besitzt, dann muss sie selbst als abstrakt gekennzeichnet werden. Tun wir das nicht, dann dürfen wir uns mal wieder die Klagen des Compilers anhören:

scala> class NotAbstract {
     |   def missingImplementation
     | }
:7: error: class NotAbstract needs to be abstract, since method missingImplementation is not defined
       class NotAbstract {
             ^

Die Methodendeklaration sieht auf den ersten Blick etwas merkwürdig aus, das liegt aber einzig allein daran, das der Rückgabetyp nicht angegeben wurde. Wir könnten auch def missingImplementation: Unit schreiben um deutlicher zu machen, dass der Rückgabewert der Methode nicht von Belang ist. Neben abstrakten Methoden kann eine Klasse auch abstrakte Variablen aufnehmen:

class Abstract {
  val someInt: Int
}
class Concrete extends Abstract {
  val someInt = 10
}

In der erbenden Klasse müssen wir die Variable einfach nochmal deklarieren und auch mit einem Wert initialisieren.

Aber kommen wir zu unserem Personen-Beispiel zurück. In den beiden Unterklassen definieren wir die Körper unserer abstrakten Klasse. Je nach dem welche Klasse wir nun instanziieren erhalten wir unterschiedliche Ausgaben:

scala> val m = new Manager
m: Manager = Manager@138524a1

scala> m.doWork()
rake in money

scala> val p = new Programmer
p: Programmer = Programmer@14d0fd23

scala> p.doWork()
write code

Nun hatte unsere Person aber einen Konstruktor, fügen wir diesen also gleich wieder hinzu:

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

scala> class Manager extends Person
<console>:8: error: not enough arguments for constructor Person: (name: String, age: Int)Person.
Unspecified value parameters name, age.
       class Manager extends Person
                     ^

scala> class Programmer extends Person
<console>:8: error: not enough arguments for constructor Person: (name: String, age: Int)Person.
Unspecified value parameters name, age.
       class Programmer extends Person
                        ^

In Scala werden Konstruktoren nicht mitvererbt. Das bedeutet, dass unsere beiden Unterklassen nur mit einem Konstruktor ohne Parameter erzeugt werden können. Da die Oberklasse aber zur Instanziierung zwei Parameter erwartet, die in den Unterklassen nirgendwo angegeben werden, erhalten wir die betreffenden Fehlermeldungen. Bleibt uns also nichts anderes übrig als auch den Unterklassen einen passenden Konstruktor zu verpassen:

scala> class Manager(val name: String, val age: Int) extends Person
<console>:8: error: not enough arguments for constructor Person: (name: String, age: Int)Person.
Unspecified value parameters name, age.
       class Manager(val name: String, val age: Int) extends Person
                    ^

Hm, die Fehlermeldung ist aber immer noch die Gleiche. Um zu erklären woher sie kommet hilft es vielleicht wenn wir uns folgendes Beispiel anschauen:

scala> class X { println("x") }
defined class X

scala> class Y extends X { println("y") }
defined class Y

scala> new X
x
res8: X = X@1f5e8c9d

scala> new Y
x
y
res9: Y = Y@42674cc4

Wir haben zwei Objekte: X und Y. Y erbt zwar von X, das heißt aber nicht, dass die Unterklasse Y die Oberklasse X komplett ersetzt. Wenn wir ein Y erzeugen wollen, dann müssen wir auch ein X erzeugen. Wir können das deutlich an der Ausgabe erkennen. Bei der Instanziierung von Y wird als erstes die Oberklasse X erzeugt, erst dann kommt Y an die Reihe. Dies ändert sich nicht wenn wir eine Klasse als abstrakt kennzeichnen. Der Compiler unterbindet uns dann zwar die Möglichkeit, dass wir die Oberklasse direkt erzeugen können – das heißt aber nicht, dass sie gar nicht erzeugt wird. Stattdessen wird sie genau dann erzeugt wenn eine der Unterklassen instanziiert wird.

Bezogen auf unser vorheriges Beispiel heißt das, dass wir als ersten eine Person erzeugen müssen bevor wir uns dem Manager oder dem Programmer zuwenden können. Die Initialisierung einer Oberklasse durch eines seiner Kinder ist in Scala denkbar einfach. Wie bei einer ganz normalen Erzeugung eines Objekts reicht es, die Initialisierungswerte in runden Klammern hinter die Oberklasse zu schreiben:

scala> class Manager(val name: String, val age: Int) extends Person(name, age)
<console>:8: error: overriding value name in class Person of type String;
 value name needs `override' modifier
       class Manager(val name: String, val age: Int) extends Person(name, age)
                         ^
<console>:8: error: overriding value age in class Person of type Int;
 value age needs `override' modifier
       class Manager(val name: String, val age: Int) extends Person(name, age)
                                           ^

Das sieht doch schon einmal ganz gut aus: Wir erhalten eine andere Fehlermeldung. Das bedeutet, dass wir der Sache also ein wenig näher kommen. Der Compiler möchte, dass wir unsere beiden Attribute mit einem override kennzeichnen. Tun wir dies hört er auf zu meckern:

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

Aber warum will er plötzlich ein override haben? Zuvor konnten wir abstrakte Methoden ja auch überschreiben, ohne dass er sich beschwert hat. Die Antwort darauf ist, dass das override immer dann gebraucht wird wenn wir bestehendes Verhalten tatsächlich auch überschreiben und nicht nur neu definieren. Bei dem Beispiel mit der Methode hatte diese keinen Körper, sie war also abstrakt und musste erst noch definiert werden. Bei dem Personen-Beispiel erzeugt uns der Compiler aber schon Getter und Setter. Die Felder wurden also schon in der Oberklasse definiert und wir wollen sie in der Unterklasse noch einmal definieren? Das geht zu recht nicht. Wir wollen sie eigentlich ja auch nicht neu definieren, sondern überschreiben. Deshalb fordert der Compiler auch das override, das ihm signalisiert, dass er das Verhalten der Oberklasse mit Verhalten aus der Unterklasse austauschen soll. Genau genommen wollen wir das ja aber eigentlich auch nicht. Wir wollen ja nur unsere Unterklassen instanziieren können ohne irgendetwas zu überschreiben.

Anmerkung:
Eine abstrakte Methode, die erst in einer Unterklasse definiert wird kann dort mit override gekennzeichnet werden, sie muss es aber nicht. Ob man es hinschreibt oder nicht bleibt einem selbst überlassen. Ich empfehle aber es zu unterlassen, da override das Überschreiben eines Members kennzeichnet und ein abstrakter Member wird ja eigentlich nicht überschrieben sondern erst einmal definiert.

Was machen wir jetzt also dagegen? Die Antwort ist ziemlich einfach, vielleicht könnt ihr sie euch schon denken:

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

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

Durch das Weglassen des val vor dem Attributnamen signalisieren wir dem Compiler, dass er keine neuen Felder erzeugen soll, sondern einfach nur einen Konstruktor mit Parameter. Aber das hatten wir schon alles.

scala> val m = new Manager("heinrich", 55)
m: Manager = Manager@75305547

scala> m.name
res10: String = heinrich

scala> m.age
res11: Int = 55

Wir können nun einen Manager erstellen und erben dabei die Getter aus der Oberklasse. Besonders toll beim Arbeiten mit Oberklassen ist, dass wir nicht wissen müssen mit welcher Kind-Klasse sie instanziiert wurde. Es wird uns erlaubt nur auf Schnittstellen-Basis zu arbeiten und konkretes Verhalten einzufordern obwohl der konkrete Typ nicht bekannt ist.

abstract class Person(val name: String, val age: Int) {
  def work()
}
class Manager(name: String, age: Int) extends Person(name, age) {
  def work() { manage() }
  def manage() { println(name+" manages his company") }
}
class Programmer(name: String, age: Int) extends Person(name, age) {
  def work() { program() }
  def program() { println(name+" programs software") }
}

Bei diesem Beispiel besitzen unsere Unterklassen je eine spezielle Methode, die die Oberklasse nicht kennt. Diese Methoden werden über die gemeinsame Schnittstelle, die Methode work, aufgerufen:

scala> val xs = List(new Manager("peter", 35), new Programmer("hugo", 42), new Manager("susie", 41))
xs: List[Person] = List(Manager@45637b37, Programmer@62e7b78, Manager@55ac0673)

scala> for (x <- xs) x.work()
peter manages his company
hugo programs software
susie manages his company

scala> xs(0).manage()
<console>:12: error: value manage is not a member of Person
              xs(0).manage()
                    ^

Wir haben mehrere Personen in einer List[Person] (deren gemeinsamer Obertyp Person vom Compiler korrekt erkannt wurde) und lassen diese alle arbeiten. Das funktioniert auch wie erwartet. Je nach dem ob wir einen Manager oder einen Programmierer erstellt haben wird die entsprechende Methode aufgerufen. Versuchen wir aber auf eine der konkreten Methoden zuzugreifen erhalten wir eine Fehlermeldung, da der Klasse Person diese ja nicht bekannt sind.

Innerhalb einer abstrakten Klasse besteht auch noch die Möglichkeit, dass wir einen bereits definierten Member nachträglich auf abstrakt setzen um dessen Überschreibung in den Unterklassen zu erzwingen:

scala> abstract class Printable {
     |   override def toString: String
     | }
defined class Printable

scala> class Test extends Printable
<console>:8: error: class Test needs to be abstract, since there is a deferred declaration of method toString in class Printable of type ()String which is not implemented in a subclass
       class Test extends Printable
             ^

Mit Hilfe der abstrakten Klasse Printable wollen wir erreichen, dass eine Klasse auf jeden Fall eine spezifische Stringrepräsentation besitzt. Durch erweitern der Klasse gehen wir sicher, dass die Methode toString auch tatsächlich überschrieben und nicht vergessen wird.

Zugriff auf die Superklassen

Wenn wir uns noch einmal das Beispiel von Setterdefinitionen anschauen stellen wir fest, dass wir unterschiedliche Namen für unsere Variablen benötigen:

class Person(val name: String, private var initAge: Int) {
  def age = initAge
  def age_=(age: Int) {
    if (age >= 0)
      initAge = age
  }
}

Wir haben hier im Konstruktor eine Variable namens initAge und im Setter eine namens age. Das ist ein wenig unhandlich. Schöner wäre es doch wenn wir beiden Variablen den gleichen Namen geben könnten. In obigem Beispiel ist das leider nicht möglich, da der Getter schon den Namen für sich beansprucht. Wir können aber die Parameternamen ändern:

class Counter {
  private var c = 0
  def count = c
  def count_=(c: Int) {
    this.c = c
  }
}

Wenn wir nun innerhalb des Setters c = c schreiben beschwert sich der Compiler weil wir den Wert der Variable sich selbst zuweisen wollen:

scala> var c = 0
c: Int = 0

scala> def change(c: Int) { c = c }
<console>:8: error: reassignment to val
       def change(c: Int) { c = c }
                              ^

Da alle Parameter in Scala als val erstellt werden, bekommen wir einen Zuweisungsfehler. Der Compiler kann ja nicht wissen, dass wir den Wert der äußeren Variable zuweisen wollen. Innerhalb einer Klasse besteht die Möglichkeit mit der this-Referenz auf den Scope der Klasse zuzugreifen und die äußere Variable also direkt anzusprechen, wie wir es beim Counter-Beispiel sehen können. Da this aber nicht auf den nächstäußeren Scope, sondern auf den der momentanen Klasse zeigt können wir damit nicht eine Variable gleichen Namens ansprechen, die sich auf einer äußeren Ebene befindet:

def x {
  var a = 0
  def y {
    var a = 3
    def z() {
      a = 5
    }
    z()
    println(a)
  }
  y
  println(a)
}

scala> x
5
0

Die erste Definition von a können wir in der innersten Methode z nicht ansprechen, da sie von dem a in Methode y verdeckt wird.

Manchmal wollen wir in einer Unterklasse eine Methode überschreiben aber gleichzeitig auch auf die Methode der Oberklasse zugreifen. Dafür gibt es dann die super-Referenz:

class Foo {
  def x() {
    println("foo")
  }
  def y(i: Int) = i*2
}

class Bar extends Foo {
  override def x() {
    super.x
    println("bar")
  }
  override def y(i: Int) = super.y(i)+10
}

Beim Aufruf der Methoden erhalten wir das erwartete Ergebnis:

scala> bar.x
foo
bar

scala> bar.y(5)
res6: Int = 20

Finale Member

Es ist nicht immer erwünscht, dass Teile unseres Codes durch Unterklassen erweitert oder sogar überschrieben werden. Nehmen wir an, eine Klasse besitzt eine Methode, die einen Parameter auf Gültigkeit überprüft. Wir wollen nicht, dass diese Überprüfung irgendjemand durch Überschreiben der Methode umgeht. Um dies nun also zu verhindern gibt es das Schlüsselwort final.

scala> class Tester {
     |   final def isValid(i: Int) = i < 5 
     | }
defined class Tester

scala> class Cracker extends Tester {
     |   override def isValid(i: Int) = true
     | }
<console>:9: error: overriding method isValid in class Tester of type (i: Int)Boolean;
 method isValid cannot override final member
         override def isValid(i: Int) = true
                      ^

Deklarieren wir eine Klasse als final können wir sogar verhindern, dass die Klasse erweitert wird:

scala> final class Tester
defined class Tester

scala> class Cracker extends Tester
<console>:8: error: illegal inheritance from final class Tester
       class Cracker extends Tester
                             ^

Installationsanleitung: Scala mit Eclipse

Ich möchte hier erklären wie ihr unter Eclipse das Scala Plugin zum Laufen bekommt. Diese Anleitung richtet sich in erster Linie an alle, die bisher noch nie mit Eclipse gearbeitet haben.

Als erstes benötigt ihr Eclipse. Ihr könnt euch die IDE unter eclipse.org/downloads besorgen. Am besten wählt ihr die „Eclipse IDE for Java Developers“ aus, die ist im Vergleich zu den anderen Versionen am leichtgewichtigsten.

Entpackt das Archiv und startet Eclipse. Es sollte nach kurzer Zeit ein Fenster mit dem Titel „Workspace Launcher“ erscheinen. Der Workspace (dt.: Arbeitsplatz) ist ein Verzeichnis auf eurem Rechner, in dem Eclipse alle anfallenden Daten abspeichert (Sourcen, Konfigurationsdatein etc.). Tragt hier irgendetwas ein und bestätigt dann den Ok-Button.

Nachdem sich das Hauptfenster geöffnet hat seht ihr erst einmal einen Willkommensdialog. Diesen könnt ihr schließen indem ihr auf das Kreuzchen im Tab klickt.

Über Windows „Help -> Install New Software“ gelangt ihr zum Install-Dialog:

Betätigt dort den „Add“-Button oben rechts. Gebt nun bei Name „Scala IDE“ und bei Location folgende Adresse ein: http://download.scala-ide.org/releases/2.0.0-beta

Wählt die ersten beiden zu installierende Pakete an und entfernt den Haken bei „Contact all update sites during install …“:

Klickt auf „Next“, dann gleich nochmal auf „Next“, akzeptiert die Lizenzen und bestätigt „Finish“. Wartet bis alle Daten heruntergeladen würden, bestätigt die Warnung und startet Eclipse neu.

Nach dem ersten Start des Scala-Plugins erscheint ein Dialog mit der Aufforderung „Setup Diagnostics“ durchlaufen zu lassen um die richtigen Einstellungen auszuwählen. Wählt in etwa dies aus:

Das Scala-Plugin benötigt ziemlich viel Speicher. Dem Plugin mehr als 1GB zuzuweisen ist nicht verkehrt. Falls ihr genügend Speicher zur Verfügung habt, dann könnt ihr diese Einstellungen in der „eclipse.ini“ vornhemen, die ihr im Eclipse-Verzeichnis vorfindet. Welche Einstellungen gesetzt werden sollen verrät euch diese Setup Seite. Sucht einfach die jeweiligen Einträge heraus und ändert sie wie beschrieben. Vergesst aber nicht zu Beginn noch eine Sicherungskopie anzufertigen. Damit neue Einstellungen aktiv werden müsst ihr Eclipse neu starten.

Nachdem ihr alle Einstellungen vorgenommen habt solltet ihr zuerst die „Scala Perspective“ auswählen, dies geschieht über „Window -> Open Perspective -> Other -> Scala“

Nun könnt ihr einen Rechtsklick auf den „Package Explorer“ machen (an der linken Seite) und „New -> Scala Project“ auswählen. Eclipse verwaltet der Übersichtlichkeit wegen euren Scala-Code in je einem eigenen Projekt. Gebt dem Projekt einen Namen und bestätigt „Finish“.

Rechtsklickt nun auf „src“ in eurem Projekt und wählt „New -> Scala Object“ aus.

Gebt dem Object einen Namen und betätigt „Finish“.

Es sollte sich nun ein Tab mit der Überschrift „HelloWorld.scala“ geöffnet haben. Fügt folgenden Quelltext ein:

object HelloWorld extends App {
  println("HelloWorld")
}

Wenn ihr nun die Tastenkombination „STRG+F11“ drückt oder den großen grünen Button (mit dem Titel Run) in der Symbolleiste betätigt sollte in der Konsole am unteren Rand der IDE der Text „HelloWorld“ erscheinen.

Glückwunsch! Ihr habt soeben euer ersten Scala-Programm geschrieben.

Und jetzt? Geht am besten gleich weiter zum Inhaltsverzeichnis um mit dem Scala-Tutorial beginnen zu können.

Teil 10: Packages, Imports und Sichtbarkeiten

Wir haben jetzt zwar schon einen Teil von Scalas objektorientierten Fähigkeiten kennen gelernt, aber noch fehlen uns noch ein paar Möglichkeiten unseren Code vernünftig zu strukturieren. Scala bietet ein sehr mächtiges Modul- und Packaging-System an, das nur wenige Wünsche offen lassen sollte.

Für diesen Teil des Tutorials werde ich von der REPL auf scalac umsteigen. Die REPL unterstützt das interpretieren von modularisiertem Code nicht, weshalb wir gar keine andere Möglichkeit haben als zum Compiler zu wechseln. Das ist aber nicht schlimm, lernen wir so doch alle Eigenschaften von Scala kennen. Wenn wir nur auf Interpreterbasis arbeiten, müssen wir neben einem fehlenden Modulsystem leider auch noch mit anderen Einschränkungen leben, was uns jetzt aber nicht weiter interessieren soll. Selbst wenn wir aber mit scalac arbeiten, hindert uns das nicht nebenher einzelne Codeteile mit der REPL zu übersetzen und sie so schnell und effizient auf ihr Funktionieren zu testen.

Es steht euch frei in nächster Zeit mit einem Editor und Konsole oder direkt mit einer IDE zu arbeiten. Unter den Links hab ich die bekanntesten IDEs aufgelistet, sucht euch euren Liebling aus.

Bevor wir loslegen noch ein paar Worte zu scalac. Der Compiler übersetzt Scala-Code in JVM-Bytecode oder CLR-CIL code. Die Unterstützung für Letzteren hat die Scala Community aber ein wenig aus den Augen verloren, weshalb es zu empfehlen ist auf der JVM zu arbeiten um alle Features auch benutzen zu können. Ich werde im Weiteren auch nicht auf die CLR eingehen und mich stattdessen nur auf die JVM konzentrieren.

Scala ist prinzipiell vollkommen kompatibel mit Java. Bis auf ein paar wenige Ausnahmen kann man in Scala alles machen was man in Java auch machen kann. Man kann Java-Bibliotheken ansprechen und mit vielen Frameworks und Tools arbeiten, die ursprünglich nur für Java gedacht waren.

Um ein Scala-Programm von der JVM starten lassen zu können benötigen wir einen Einsprungpunkt. Wie bei vielen Programmiersprachen üblich ist das in Scala eine Methode namens main:

object Main {
  def main(args: Array[String]) {
    println("Hello World")
  }
}

Wir müssen den Code in eine Datei mit der Endung scalaplatzieren. Der Name der Datei ist egal, ich empfehle aber sie so zu nennen wie die Klasse, die die Datei beinhaltet.

Übersetzen können wir den Code ganz einfach mit

scalac <name_of_file>

und ausführen mit

scala <name_of_object>

Ein Beispiel:

$ scalac Main.scala
$ scala Main
Hello World

Wie ihr beim Ausführen von scalac sicherlich merkt dauert das einfach nur ewig. Das liegt wieder daran, dass zuerst die JVM gestartet werden muss. Um die Kompilierungsgeschwindigkeit zu erhöhen empfiehlt sich beim Arbeiten auf der Konsole der Einsatz von fsc, dem Fast Scala Compiler:

$ fsc Main.scala
$ scala Main
Hello World

Bei erneuter Kompilierung erhalten wir deutlich schneller ein Ergebnis.

Wenn wir den Code mit scala ausführen wollen ist es essentiell wichtig, dass wir den Namen des object eingeben, das die main-Methode beinhaltet. Die Dateiendung class darf nicht angegeben werden. Unabhängig vom Namen der Quelldatei generiert scalac Dateien mit Namen der benötigten Klassen. Je nach dem wie viel Code wir schreiben kann es also schon mal sein, dass auf eine Scala-Datei dutzende Bytecode-Dateien kommen.

Seit Scala 2.9 wird noch eine weitere Möglichkeit unterstützt Scala-Code mit scalac zu starten:

object Main extends App {
  println("Hello World")
}

Hier erbt unser object vom object App, das eine main-Methode beinhaltet. Falls wir nicht auf die main-Methode angewiesen sind erlaubt uns die Erweiterung von App den auszuführenden Code direkt in den Rumpf des object zu schreiben. Aber kommen wir nun zum eigentlichen Inhalt des Artikels.

Imports

Das import-Schlüsselwort haben wir schon kennen gelernt. Wir können es überall im Code platzieren, auch innerhalb von Klassen und Methoden und so den Wirkungsbereich des Imports einschränken. Wollen wir einen Import in der kompletten Datei erlauben, müssen wir sie direkt zu Beginn der Datei aufschreiben.

import scala.math.sqrt

object Main extends App {
  println(sqrt(1234))
}

Die einfachste import-Anweisung erstellt man, indem man einfach den Namen der zu importierenden Einheit angibt. Wollen wir mehrere Dinge aus dem gleichen Package importieren stellt uns Scala drei Schreibweisen zur Verfügung:

// 1
import scala.math.sqrt
import scala.math.pow

// 2
import scala.math.sqrt, scala.math.pow

// 3
import scala.math.{ sqrt, pow }

Um uns Schreibarbeit zu ersparen dürfen wir Importe mit einem Komma trennen oder sie mit geschweiften Klammern gruppieren. Die letzte Möglichkeit hat den Vorteil, dass wir bei gleichen Packagenamen nicht mehr den kompletten Pfad angeben müssen, sondern nur noch die zu importierenden Einheit. Wir haben auch die Möglichkeit alle Inhalt eines Package zu importieren:

import scala.math._

Der Unterstrich dient hier als Wildcard-Symbol. Gefällt uns der Name eines importierten Inhalts nicht oder treten Namenskonflikte auf können wir sogar temporäre Umbenennungen vornehmen:

import scala.math.{ sqrt => squareRoot }

Das =>-Symbol erlaubt uns einen anderen Namen für den Import auszuwählen. Innerhalb des Sichtbereichs des Imports können wir fortan die Methode sqrt nur noch mit dem Identifier squareRoot ansprechen. Zu beachten ist noch, dass die Umbenennung zwingend in geschweiften Klammern stehen muss, wir dürfen aber innerhalb dieses Blocks beliebig viele Umbenennungen vornehmen, wir müssen sie nur wieder mit einem Komma trennen:

import scala.math.{ sqrt => squareRoot, pow => mathpow }

Wir können aber nicht nur etwas importieren, wir können den Import auch wieder aufheben. Das macht vor allem dann Sinn wenn wir alle Inhalte eines Package mit dem Unterstrich importieren aber einzelne Member eben nicht haben wollen:

import java.lang.reflect._
object Main {
  def main(args: Array[String]) {}
}

Dieser Code würde nicht kompilieren. Warum? Die Reflection-Library von Java besitzt ebenfalls eine Klasse Array, die die Scala Klasse überschreibt. Es zählt immer nur der Name des zuletzt importierten Members und da scala.Array vor dem Java Pendant importiert wird können wir nicht mehr darauf zugreifen. Da java.lang.reflect.Array nicht parametisiert ist bekommen wir vom Compiler eine Fehlermeldung – falls es parametisiert wäre würde der Code anstandslos kompilieren. Er würde aber nicht ausgeführt werden können weil die main-Methode eine andere Signatur besitzt als von scala (dem Tool) erwartet.

Wir können nun scala.Array mit vollem Namen adressieren, oder aber wir importieren java.lang.reflect.Array gar nicht erst:

import java.lang.reflect.{ Array => _, _ }

Wenn wir bei einer Umbenennung einen Unterstrich verwenden heißt das, dass wir den Member fortan nicht mehr ansprechen können. Der zweite Unterstrich heißt weiterhin „importiere alles“.

Nun gibt es noch ein paar Sachen über Imports, über die wir Bescheid wissen sollten. So ist es möglich nicht nur Member, sondern auch Packages zu importieren:

scala> import scala.collection.mutable
import scala.collection.mutable

scala> val xs = mutable.Buffer(1, 2, 3)
xs: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1, 2, 3)

scala> xs += 4
res10: xs.type = ArrayBuffer(1, 2, 3, 4)

Dies ermöglicht uns einen Typ direkt als veränderlich zu kennzeichnen, ohne dass wir ihn umbenennen (z.B. in MutableBuffer) oder beim kompletten Package-Namen nennen müssten.

Wissenswert ist auch noch, dass alle Member aus den Packages scala, scala.Predef und java.lang automatisch importiert werden und zwar als die ersten Imports überhaupt. Sie müssen deshalb nicht extra importiert werden und da scala ein Package ist, das importiert wird, müssen wir es auch nicht unbedingt angeben:

import collection.mutable
import math.sqrt

Besonders wichtig ist es, zu wissen, dass in Scala alle Importe relativ und nicht absolut sind. Das bedeutet, dass wir einen Import abhängig vom Vorherigen machen:

// 1
import scala.collection.{ mutable, immutable }
import mutable.Buffer
import immutable.SortedSet

// 2
import scala.collection.JavaConversions
import JavaConversions._

Wie zu erkennen, genügt es, einen Member eines Packages oder einer Klasse direkt anzusprechen wenn wir zuvor bereits die nötigen Importe getätigt haben. Das ist vor allem dann nützlich wenn wir auf mehrere Packages unterschiedlichen Namens zugreifen möchten ohne alle mit vollem Namen zu adressieren (wie bei 1 zu sehen). Ebenso nützlich ist das wenn wir nicht nur eine Klasse/ein Object importieren möchten, sondern auch dessen Member (wie bei 2). Die Nützlichkeit dieser beiden Featuers verliert mit besserer IDE-Unterstützung ein wenig an Bedeutung, da die IDE die Importe für uns erledigen kann. Dennoch wird es immer den ein oder anderen Anwendungsfall geben bei dem es nützlich ist auf dieses Feature zurückgreifen zu können.

Sobald eine import-Anweisung innerhalb eines Packages nicht mehr gefunden werden kann wird wieder vom root-Pfad ausgegangen und von dort importiert. Dies geschieht beim Wechsel zwischen 1 und 2. Manchmal kann es aber zu Konflikten kommen wenn ein Unterpackage gleich heißt wie ein höher liegendes:

package de {
  package bar {
    package de {
      package num {}
    }
  }
  package foo {}
}

import de.bar.de
import de.num
import de.foo // error

Der letzte Import würde nicht erkannt werden, da der Compiler versucht das Package de.foo über de.bar.de.foo zu finden was fehl schlägt. Hierfür bietet Scala das _root_-Package welches eine absolute Adressierung erlaubt:

import _root_.de.foo

Um diesen Konflikt zu vermeiden empfiehlt es sich, allen Packages möglichst einzigartige Namen zu geben. Packagenamen wie scala oder math sollte man grundsätzlich nicht verwenden, da die Standardpackages so heißen und es deshalb nur Probleme geben kann.

Packages

Wir haben nun kennen gelernt wie wir Packages importieren, schauen wir uns also an wie wir eigene erstellen können.

package test.hello.world

Die Package-Anweisung muss die erste Anweisung einer Datei sein, sie kommt also noch vor den Imports. Eingeleitet wird eine Package-Deklaration mit dem Schlüsselwort package. Danach folgt das oder folgen die Packages, getrennt durch einen Punkt. Im obigem Beispiel würden wir die Packages test, hello und world erstellen. Dabei gilt, dass die Packages ineinander verschachtelt sind. D.h. dass sich hello innerhalb von test befindet und world befindet sich innerhalb von hello. Wollen wir auf ein Member in world zugreifen müssen wir es über den kompletten Pfad ansprechen:

import test.hello.world

Der Scala-Compiler mappt die Package-Struktur bei der Kompilierung auf das Verzeichnissystem, was bedeutet, dass er die Verzeichnishierarchie test/hello/world erzeugen und alle Member von world dort ablegen würde. Die Quelldateien selbst müssen sich dagegen nicht ebenso zwingend in der selben Hierarchie befinden. Es wäre möglich alle Dateien in ein Verzeichnis zu platzieren und dem Compiler die Erstellung der Pfade zu überlassen. Da der Compiler aber nur die Pfade für unseren Bytecode erstellt, würden wir damit erhebliche Übersichtlichkeitsmängel hinnehmen, besonders bei größeren Programmen. Ich empfehle also für jedes erstellte Package auch ein eigenes Verzeichnis anzulegen. Wenn wir mit einer IDE arbeiten geschieht dies sowieso automatisch – wir brauchen uns darum also gar nicht kümmern.

Neben dem Trennen von Packages über die Punktnotation können wir die Packages auch mit geschweiften Klammern ineinander verschachteln:

package test {
  package hello {
    package world {
      package bar {}
    }
  }
  package foo {}
}

Die beiden Notationen unterscheiden sich aber ein wenig bezüglich automatischer Imports. In erstgenannter Notation wird ein Package-Member ins Package test.hello.world integriert und alle Member aus world werden automatisch importiert. Bei der verschachtelten Notation sieht das mit der Notation allerdings ein wenig anders aus. Hier werden alle Member aus allen explizit angegebenen Packages importiert.

Wir können also folgenden Code ohne Probleme übersetzen:

package test {
  package hello {
    package world {
      package bar {
        class A {
          new B // B is implicitly imported
        }
      }
      class B
    }
  }
  package foo {}
}

Dieser dagegen würde Fehler verursachen, da der Compiler die Klasse B nicht auflösen kann:

package test {}
package test.foo {}
package test.hello {}
package test.hello.world {
  class B
}
package test.hello.world.bar {
  class A {
    new B // error, no implicit import
  }
}

Erst durch den Import von B können wir den Fehler beheben:

class A {
  import test.hello.world._
  new B
}

Das gleiche macht auch der Compiler: einen impliziten Import aller Member aller höher liegenden Packages. Wir können auch mehrerer solcher Package-Anweisungen zu Beginn unserer Datei schreiben damit wir benötigte Imports nicht mehr explizit angeben müssen:

package test.hello
package world

// implicit import of test.hello._
// implicit import of test.hello.world._
// members of test are NOT visible

// class A is in package test.hello.world
class A

Die Frage ist jetzt welche Schreibweise man bevorzugen sollte? Die verschachtelte Schreibweise hat den Nachteil, dass sie mehr Schreibarbeit erfordert und die Übersichtlichkeit stören kann wenn mehrere Klassen, in einer Datei zusammengefasst werden. Wenn die jeweiligen Klassen nicht direkt zusammen gehören macht es mehr Sinn sie auf verschiedene Dateien zu verteilen um die Übersichtlichkeit zu bewahren. Mehrfache Package-Anweisungen ohne verschachtelte Notation machen aber dann Sinn wenn auf Member einer äußeren Package-Schicht zugegriffen werden muss und man keine Imports angeben möchte.

Scala unterstützt noch sogenannte Package Objects. Diese ermöglichen den Zugriff auf die Member eines Objects indem man einfach nur das Package importiert:

// file test/hello/package.scala
package test.hello
package object world {
  def printHelloWorld() { println("hello world") }
}

// file any/test/Test.scala
package any.test
import test.hello.world._
object Test extends App {
  printHelloWorld()
}

Im Object Test können wir über einen ganz normal aussehenden Package-Import, die Methode printHelloWorld importieren. Hätten wir die Methode innerhalb des Packages test.hello.world in einem weiteren Object platziert, müssten wir, zusätzlich zu den Membern aus dem Package, auch noch das Object importieren, was mehr Arbeitsaufwand bedeuten würde (und wir wollen ja nicht arbeiten ;)).

Zur Übersichtlichkeit empfehle ich, ein Package Object immer mit package.scala zu benennen, dann findet man es auch gleich wenn man es mal suchen sollte.

Besonders zu beachten ist, dass ein Package Object eine weitere Package-Anweisung ist, d.h. wir müssen auf die richtige Packageumgebung achten. In obigen Beispiel liegt das package in test.hello und nicht in test.hello.world! Würden wir es in Letzterem platzieren müssten wir es über test.hello.world.world._ importieren, was nicht der Sinn der Sache wäre.

Anmerkung: Mit der Anweisung

import test.hello.world.`package`._

ist es möglich auch nur das Package Object zu importieren (Backticks beachten). Das dürften wir aber wohl nie wollen, da es ja gerade der Sinn eines Package Objects ist nicht explizit importiert zu werden.

Sichtbarkeiten

Kommen wir zum letzten Teil dieses Kapitels, den Sichtbarkeiten oder auch Zugriffsmodifizierern. In Scala gibt es drei mögliche Arten von Sichtbarketien: öffentlich, privat und geschützt. Die erste Variante ist der Standard und erfordert deshalb keine Anweisung. Jeder Member, egal ob Object, Klasse, Methode oder Variable, die ohne einen Zugriffsmodifizierer deklariert wurden, sind von Haus aus öffentlich. Wir können sie dann von jedem beliebigen Package aus ohne Einschränkungen aufrufen – wir müssen sie nur importieren. Die private Sichtbarkeitsstufe wird durch das Schlüsselwort private erstellt. Das bedeutet, dass der deklarierte Member fortan nur noch innerhalb des deklarierten Bereichs sichtbar ist.

class A {
  private def x = ...
  def y = ...
}

Außerhalb von A könnten wir auf y zugreifen nicht aber auf x. Einzige Ausnahme bildet das Companion Object von A. Existiert eines, kann es auf die privaten Member von A zugreifen. Wird eine Klasse oder ein Object mit private gekennzeichnet, dann ist es nur innerhalb des jeweiligen Packages sichtbar.

Der letzte Zugriffsmodifizierer, protected, besagt, dass ein Member nur innerhalb des deklarierten Bereichs und aller erbenden Bereiche sichtbar ist.

class A {
  protected def x = ...
}
class B extends A {
  // x is accessible
}

Alle erbenden Klassen genießen das Privileg auf die mit protected markierten Member zugreifen zu dürfen. Das Companion Object bildet wie immer eine Ausnahme. In diesem Fall dürfen sogar zwei Companion Objects auf xzugreifen, das von A und das von B:

class A {
  protected def x = 0
}

object A {
  new A().x
}

class B extends A {
  x
}

object B {
  new B().x
}

Eine Klasse oder ein Object mit protected zu kennzeichnen macht keinen Sinn, es sei denn es wurde innerhalb einer anderen Klasse/eines anderen Objects deklariert.

protected class A // senseless, same as private
class B {
  protecded class C // ok
}

Innere Klassen oder Objekt werden ganz normal mitvererbt wenn sie mit protected gekennzeichnet wurden.

Sollten die drei Modifizierer mal nicht ausreichen gibt es noch eine sogenannte Package-Sichtbarkeit. Diese können wir erreichen indem wir hinter ein private oder ein protected in eckigen Klammern einen Package-Namen schreiben:

package test.hello.world
private[world] class A
// both means the same
private class A

Das Angeben eines Package-Namens bedeutet, dass der Member innerhalb des Packages und aller Unterpackages sichtbar ist. In obigem Beispiel kann man die Package-Angabe weglassen, da sie für das gleiche Package gilt in der die Klasse sowieso schon sichtbar ist. Das ganze ist dann nützlich wenn wir ein Member einem höher liegenden Package sichtbar machen wollen:

package test.hello.world
private[hello] class A

Auf die Klasse könnte jetzt zusätzlich noch von allen Membern aus dem Package hello und all dessen Unterpackages zugegriffen werden. Package-Angaben funktionieren sowohl bei private als auch bei protected, nicht aber bei öffentlichen Membern. Dort macht es auch keinen Sinn, da sowieso schon jeder darauf zugreifen kann.

Als Package-Angabe darf auch die this-Referenz, angegeben werden. Sie besagt dass auf einen Member nur vom gleichen Objeckt aus zugegriffen werden darf.

class A {
  private[this] def x = 0

  def y = new A().x // error
}

object A {
  new A().x // error
}

Dies stellt eine Möglichkeit dar ein Feld selbst vor dem Companion Object abzuschotten. Es macht aber wohl nicht viel Sinn dem Companion Object den Zugriff zu verbieten, wichtiger dürfte es in Verbindung mit veränderlichen Daten werden:

class IntContainer(e: Int) {
  private var elem = e

  def incSame() = {
    elem += 1
    this
  }

  def incNew() = {
    val c = new IntContainer(elem)
    c.elem += 1
    c
  }
}

Außerhalb der Klasse gibt es keine Möglichkeit auf elem zuzugreifen. Innerhalb von incNew könnten aber Leichtsinnsfehler beim Ändern des Feldes passieren. Würden wir das Feld mit private[this] sichern würde der Code nicht mehr kompilieren.
Das Beispiel mag ein wenig an den Haaren herbeigezogen sein, wenn wir aber irgendwann mit Varianzen arbeiten werden wir ein Beispiel kennen lernen bei dem es Sinn macht Attribute einer Klasse nach außen hin komplett abzuschotten.

Rational goes private

Es wird nun Zeit unser eben erlerntes Wissen auf unsere Klasse Rational anzuwenden. Stecken wir die Klasse erst in ein Package, z.B.:

package extendedmath
object Rational ...
class Rational ...

Als nächsten wollen wir alle Member nach außen hin abschotten, auf die man von außen nicht zugreifen können soll. Das wären die Methode gcd und das Feld g:

// in class Rational
private val g = gcd(numerator, denominator)
private def gcd(a: Int, b: Int): Int = ...

Weiterhin könnte es nützlich sein wenn wir die Erzeugung eines Rational-Objektes direkt über den Konstruktor verbieten würden. Wir setzten also auch ihn auf private:

class Rational private(numerator: Int, denominator: Int) ...

Da wir von außen nun nicht mehr auf den Konstruktor zugreifen können, können wir auch den optionalen Parameter wieder entfernen.

scala> Rational(7, 12)
res3: Rational = 7/12

scala> res3.g
<console>:13: error: value g in class Rational cannot be accessed in Rational
              res3.g
                   ^

scala> new Rational(5)
<console>:13: error: constructor Rational in class Rational cannot be accessed in object $iw
              new Rational(5)
              ^

Sieht doch gut aus, oder? Da es nun nicht mehr möglich ist direkt auf den Konstruktor zuzugreifen können wir die Überprüfung des Nenners und die Kürzung des Bruchs auch in das Companion Object verschieben. Das sieht dann so aus:

object Rational {

  def apply(numerator: Int, denominator: Int = 1) = {
    require(denominator != 0)

    val g = gcd(numerator, denominator)
    new Rational(numerator / g, denominator / g)
  }

  private 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))
  }
}

class Rational private(val n: Int, val d: Int) {

  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
}

Und zum testen ob es funktioniert:

scala> Rational(1599, 2091)
res9: Rational = 13/17

scala> res9 + Rational(5, 7)
res10: Rational = 176/119

scala> Rational(3) + Rational(5) * Rational(4)
res11: Rational = 23/1

Sogar die Operatorpriorität wurde korrekt erkannt, wieso habe ich hier erklärt. Ob es nun sinnvoll ist, Überprüfungen von Attributen und etwaige Initialisierungsroutinen innerhalb eines Klassenrumpfes oder im Companion Object zu machen kommt wohl ganz auf den Anwendungsfall an. Bei uns würde beides gehen und keine der Vorgehensweisen hätte einen entscheidenden Vor- oder Nachteil.