Scala : objektfunktionale Programmierung

330
SCALA oliver BRAUN OBJEKTFUNKTIONALE PROGRAMMIERUNG

Transcript of Scala : objektfunktionale Programmierung

Page 1: Scala : objektfunktionale Programmierung

SCALA oliver BRAUN

OBJEKTFUNKTIONALE PROGRAMMIERUNG

Page 2: Scala : objektfunktionale Programmierung

Braun

Scala

Bleiben Sie einfach auf dem Laufenden: www.hanser.de/newsletterSofort anmelden und Monat für Monatdie neuesten Infos und Updates erhalten.

v

Page 3: Scala : objektfunktionale Programmierung
Page 4: Scala : objektfunktionale Programmierung

Oliver Braun

ScalaObjektfunktionale Programmierung

Page 5: Scala : objektfunktionale Programmierung

Prof. Dr. Oliver Braun, Mü[email protected]

Alle in diesem Buch enthaltenen Informationen, Verfahren und Darstellungen wurden nachbestem Wissen zusammengestellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen. Aus diesem Grund sind die im vorliegenden Buch enthaltenen Informationen mitkeiner Verpflichtung oder Garantie irgendeiner Art verbunden. Autor und Verlag übernehmeninfolgedessen keine juristische Verantwortung und werden keine daraus folgende oder sonstigeHaftung übernehmen, die auf irgendeine Art aus der Benutzung dieser Informationen – oderTeilen davon – entsteht, auch nicht für die Verletzung von Patentrechten und anderen RechtenDritter, die daraus resultieren könnten. Autor und Verlag übernehmen deshalb keine Gewährdafür, dass die beschriebenen Verfahren frei von Schutzrechten Dritter sind.Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesemBuch berechtigt deshalb auch ohne besondere Kennzeichnung nicht zu der Annahme, dasssolche Namen im Sinne der Warenzeichen und Markenschutz Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.

Bibliografische Information der Deutschen Nationalbibliothek:Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.

Dieses Werk ist urheberrechtlich geschützt.Alle Rechte, auch die der Übersetzung, des Nachdruckes und der Vervielfältigung des Buches,oder Teilen daraus, vorbehalten. Kein Teil des Werkes darf ohne schriftliche Genehmigung desVerlages in irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren) – auch nicht fürZwecke der Unterrichtsgestaltung – reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden.

© 2011 Carl Hanser Verlag München (www.hanser.de)Lektorat: Margarete MetzgerHerstellung: Irene WeilhartCopy editing: Jürgen Dubau, Freiburg/ElbeUmschlagdesign: Marc Müller Bremer, www.rebranding.de, MünchenUmschlagrealisation: Stephan RönigkDatenbelichtung, Druck und Bindung: Kösel, KrugzellAusstattung patentrechtlich geschützt. Kösel FD 351, Patent Nr. 0748702Printed in Germany

ISBN 978 3 446 42399 2

Page 6: Scala : objektfunktionale Programmierung

für Mia

Page 7: Scala : objektfunktionale Programmierung
Page 8: Scala : objektfunktionale Programmierung

Inhaltsverzeichnis

Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XI

1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.1 Was Führungskräfte über Scala wissen sollten . . . . . . . . . . . . 31.2 Java-Scala-Integration . . . . . . . . . . . . . . . . . . . . . . . . . . 41.3 Über dieses Buch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51.4 Typographische und sonstige Konventionen . . . . . . . . . . . . . 6

2 Einrichten der Arbeitsumgebung . . . . . . . . . . . . . . . . . . . . . . 92.1 Die Scala-Shell und die Kommandozeile . . . . . . . . . . . . . . . . 9

2.1.1 Der Scala-Interpreter . . . . . . . . . . . . . . . . . . . . . . . 112.1.2 Die Scala-(De-)Compiler . . . . . . . . . . . . . . . . . . . . . 132.1.3 Der Dokumentationsgenerator . . . . . . . . . . . . . . . . . 16

2.2 Buildtools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172.2.1 Das Maven-Scala-Plugin . . . . . . . . . . . . . . . . . . . . . 172.2.2 Simple Build Tool . . . . . . . . . . . . . . . . . . . . . . . . . 19

2.3 IDE-Support . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222.3.1 Eclipse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222.3.2 NetBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232.3.3 IntelliJ IDEA . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

3 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273.1 Ein kleines bisschen Syntax . . . . . . . . . . . . . . . . . . . . . . . 273.2 Imperative Programmierung . . . . . . . . . . . . . . . . . . . . . . 393.3 Ein ausführbares Programm . . . . . . . . . . . . . . . . . . . . . . . 423.4 Annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44

4 Reine Objektorientierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 474.1 Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

Page 9: Scala : objektfunktionale Programmierung

VIII Inhaltsverzeichnis

4.1.1 Felder und Methoden . . . . . . . . . . . . . . . . . . . . . . 474.1.2 Was Klassen sonst noch enthalten können . . . . . . . . . . . 604.1.3 Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . 624.1.4 Enumerations . . . . . . . . . . . . . . . . . . . . . . . . . . . 654.1.5 Vererbung und Subtyping . . . . . . . . . . . . . . . . . . . . 674.1.6 Abstrakte Klassen . . . . . . . . . . . . . . . . . . . . . . . . 73

4.2 Codeorganisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 784.2.1 Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 784.2.2 Package Objects . . . . . . . . . . . . . . . . . . . . . . . . . . 794.2.3 Importe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80

4.3 Traits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 824.3.1 Rich Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . 834.3.2 Stapelbare Modifikationen . . . . . . . . . . . . . . . . . . . . 88

4.4 Implicits und Rich-Wrapper . . . . . . . . . . . . . . . . . . . . . . . 95

5 Funktionales Programmieren . . . . . . . . . . . . . . . . . . . . . . . . . 1015.1 Lazy Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1025.2 Funktionen und Rekursionen . . . . . . . . . . . . . . . . . . . . . . 1045.3 Higher-Order-Functions . . . . . . . . . . . . . . . . . . . . . . . . . 1085.4 Case-Klassen und Pattern Matching . . . . . . . . . . . . . . . . . . 114

5.4.1 Case-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . 1195.4.2 Versiegelte Klassen . . . . . . . . . . . . . . . . . . . . . . . . 1225.4.3 Partielle Funktionen . . . . . . . . . . . . . . . . . . . . . . . 1245.4.4 Variablennamen für (Teil-)Pattern . . . . . . . . . . . . . . . 1265.4.5 Exception Handling . . . . . . . . . . . . . . . . . . . . . . . 1265.4.6 Extraktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1285.4.7 Pattern Matching mit regulären Ausdrücken . . . . . . . . . 130

5.5 Currysierung und eigene Kontrollstrukturen . . . . . . . . . . . . . 1325.6 For-Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1415.7 Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

5.7.1 Standardtypen . . . . . . . . . . . . . . . . . . . . . . . . . . 1475.7.2 Parametrischer Polymorphismus und Varianz . . . . . . . . 1485.7.3 Upper und Lower Bounds . . . . . . . . . . . . . . . . . . . . 1515.7.4 Views und View Bounds . . . . . . . . . . . . . . . . . . . . . 1545.7.5 Context Bounds . . . . . . . . . . . . . . . . . . . . . . . . . . 1555.7.6 Arrays und @specialized . . . . . . . . . . . . . . . . . . . . 1555.7.7 Generalized Type Constraints . . . . . . . . . . . . . . . . . . 1585.7.8 Self-Type-Annotation . . . . . . . . . . . . . . . . . . . . . . 160

Page 10: Scala : objektfunktionale Programmierung

Inhaltsverzeichnis IX

5.7.9 Strukturelle und existenzielle Typen . . . . . . . . . . . . . . 162

6 Die Scala-Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . . . 1656.1 Überblick und das Predef-Objekt . . . . . . . . . . . . . . . . . . . 1656.2 Das Collection-Framework . . . . . . . . . . . . . . . . . . . . . . . . 1706.3 Scala und XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1766.4 Parser kombinieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1806.5 Ein kleines bisschen GUI . . . . . . . . . . . . . . . . . . . . . . . . . 187

7 Actors – Concurrency und Multicore-Programmierung . . . . . . . . . . 1937.1 Ein Thread ist ein Actor . . . . . . . . . . . . . . . . . . . . . . . . . 1947.2 Empfangen und Reagieren . . . . . . . . . . . . . . . . . . . . . . . . 1967.3 Dämonen und Reaktoren . . . . . . . . . . . . . . . . . . . . . . . . . 2077.4 Scheduler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2097.5 Remote Actors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211

8 Softwarequalität – Dokumentieren und Testen . . . . . . . . . . . . . . 2158.1 Scaladoc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2168.2 ScalaCheck . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221

8.2.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2218.2.2 Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2248.2.3 Automatisiertes Testen mit Sbt . . . . . . . . . . . . . . . . . 229

8.3 ScalaTest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2328.3.1 ScalaTest und JUnit . . . . . . . . . . . . . . . . . . . . . . . . 2338.3.2 ScalaTest und TestNG . . . . . . . . . . . . . . . . . . . . . . 2358.3.3 ScalaTest und BDD . . . . . . . . . . . . . . . . . . . . . . . . 2368.3.4 Funktionale, Integrations- und Akzeptanztests . . . . . . . . 2388.3.5 Die FunSuite . . . . . . . . . . . . . . . . . . . . . . . . . . . 240

8.4 Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2418.4.1 Eine Specs-Spezifikation . . . . . . . . . . . . . . . . . . . . . 2428.4.2 Matchers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2448.4.3 Mocks mit Mockito . . . . . . . . . . . . . . . . . . . . . . . . 2488.4.4 Literate Specifications . . . . . . . . . . . . . . . . . . . . . . 249

9 Webprogrammierung mit Lift . . . . . . . . . . . . . . . . . . . . . . . . 2539.1 Quickstart mit Lift . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2549.2 Bootstrapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2579.3 Rendering – Templates und Snippets . . . . . . . . . . . . . . . . . . 2629.4 Benutzerverwaltung und SiteMap . . . . . . . . . . . . . . . . . . . 264

Page 11: Scala : objektfunktionale Programmierung

X Inhaltsverzeichnis

9.5 Persistenz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2669.6 Implementierung der Snippets . . . . . . . . . . . . . . . . . . . . . 269

10 Leichtgewichtige Webprogrammierung mit Scalatra . . . . . . . . . . . 27910.1 Quickstart mit Scalatra . . . . . . . . . . . . . . . . . . . . . . . . . . 27910.2 Der Final-Grade-Calculator . . . . . . . . . . . . . . . . . . . . . . . 281

11 Akka – Actors und Software Transactional Memory . . . . . . . . . . . 28711.1 Quickstart mit Akka . . . . . . . . . . . . . . . . . . . . . . . . . . . 28811.2 Der MovieStore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28911.3 User- und Session-Management . . . . . . . . . . . . . . . . . . . . . 29311.4 Software Transactional Memory . . . . . . . . . . . . . . . . . . . . . 29711.5 Client und Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300

Schlusswort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305

Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309

Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311

Page 12: Scala : objektfunktionale Programmierung

Vorwort

Scala: Eine Programmiersprache, die auf einzigartige Weise die objektorientierteProgrammierung mit der funktionalen1 verschmilzt, die sich anschickt, Java vomThron zu stoßen, und mit der es richtig Spaß macht zu programmieren.Im Sommersemester 2010 habe ich erstmals neben Haskell auch Scala in der Vor-lesung „Fortgeschrittene funktionale Programmierung” vorgestellt und viele er-freuliche Erfahrungen gesammelt. Ich erlebte, wie meine Studierenden die ver-mittelten funktionalen Konzepte mit Scala sehr gut auf die JVM übertragen konn-ten. Ab dem Wintersemester 2010/11 begann ich damit, Scala statt Java für dieProgrammierausbildung der Studienanfänger zu verwenden. In Scala sind vie-le Dinge einfacher und sauberer umgesetzt worden. Beispielsweise lässt sich derAusdruck println("Hello World") ganz alleine in einer Datei als Script aus-führen oder direkt in den interaktiven Scala-Interpreter eintippen. In Scala kön-nen wir dann zunächst so nah an Java bleiben, dass ein späterer Umstieg kaumProbleme bereiten dürfte2. Dass ich Scala auch für die Programmierung verteilterSysteme bespreche, versteht sich aufgrund der Actors und Akka sicher von selbst.Was Scala so alles zu bieten hat, möchte ich Ihnen mit diesem Buch näherbringen,in dem ich Ihnen neben der Programmiersprache selbst auch die wesentlichenTools und Frameworks vorstellen werde.Mein herzlichster Dank gilt allen, die dieses Buch ermöglicht und mich in diesemProjekt unterstützt haben: Allen voran meine Familie. Wichtige Diskussionen undAnmerkungen haben Patrick Baumgartner, Jürgen Dubau, Christoph Schmidt,Heiko Seeberger und Bernd Weber beigesteuert. Vielen Dank dafür. Für die gu-te Zusammenarbeit mit dem Hanser Verlag danke ich stellvertretend MargareteMetzger und Irene Weilhart.Happy Scala Hacking wünscht

Oliver BraunMünchen, Oktober 2010

1 Dass sich die funktionale Programmierung langsam aber sicher in den Mainstream vorarbeitet, mussich hier sicher nicht erwähnen.2 . . . und sobald wir die weitergehenden Konzepte besprechen, will sowieso keiner mehr von Scalaweg ;-).

Page 13: Scala : objektfunktionale Programmierung
Page 14: Scala : objektfunktionale Programmierung

Kapitel 1

Einführung

Schon wieder eine neue Programmiersprache, obwohl: Scala ist gar nicht mehr soneu. Die Entwicklung begann 2001 an der École polytechnique fédérale de Lau-sanne (EPFL) in der Schweiz von einem Team um Professor Martin Odersky. Daserste Release wurde bereits 2003 veröffentlicht. 2006 folgte die Version 2.0 und2010, gerade während dieses Buch geschrieben wird, wird die Version 2.8 freige-geben, die laut Odersky eigentlich 3.01 heißen müsste.Professor Odersky ist in der Java-Welt kein Unbekannter. 1995 begann er mit Phi-lip Wadler2 mit der Entwicklung der funktionalen Programmiersprache Pizza3,die sich in Bytecode für die Java Virtual Machine (JVM) übersetzen lässt. DieseArbeit führte über GJ4 schließlich zum neuen javac-Compiler und den mit Java5 eingeführten Java Generics5.Als Odersky 1999 an die EPFL kam, verschob er seinen Fokus etwas. Er hatteimmer noch das Ziel, die objektorientierte und funktionale Programmierung zuverbinden, wollte sich aber nicht mehr mit den Restriktionen von Java belasten.Nach der Entwicklung der Programmiersprache Funnel6 nahm er als weitere Zieledie praktische Verwendbarkeit und die Interoperabilität mit Standardplattformenhinzu und entwarf Scala7.Scala ist eine Hybrid-Sprache, die auf einzigartige Weise Features von objektori-entierten und funktionalen Programmiersprachen verbindet. Designziel von Scala

1 Eigentlich sollte nur das Collection-Framework (siehe Abschnitt 6.2) einem Redesign unterworfenwerden. Die Arbeiten daran dauerten aber etwa 18 Monate, sodass parallel dazu eine Vielzahl ande-rer Features entstanden, die in die bereits angekündigte Version 2.8 aufgenommen wurden. Von derbereits kommunizierten Versionsnummer wollte das Scala-Team nicht mehr abweichen.2 http://homepages.inf.ed.ac.uk/wadler/3 siehe [OW97] und http://pizzacompiler.sourceforge.net/4 siehe [BOSW98] und http://lamp.epfl.ch/pizza/gj/5 siehe [NW06]6 siehe [Ode00] und http://lampwww.epfl.ch/funnel/7 siehe [Ode10b] und http://www.scala-lang.org/

Page 15: Scala : objektfunktionale Programmierung

2 1 Einführung

ist eine knappe, elegante und typsichere Programmierung. Scala wird nicht nur inBytecode für die JVM kompiliert, es lässt sich auch jeglicher Java-Code direkt ausScala heraus nutzen und umgekehrt.Scala ist eine rein objektorientierte Programmiersprache. Das heißt, in Scala ist je-der Wert ein Objekt. Scala nutzt ein Konzept von Klassen und Traits. Mit Traitslassen sich Rich-Interfaces realisieren, denn Traits können bereits konkrete Imple-mentierungen enthalten. Klassen werden durch Vererbung erweitert, Traits wer-den in eine Klasse oder in ein Objekt hineingemixt. Um nicht in typische Problememit Mehrfachvererbung zu laufen, werden Traits linearisiert.Ob Scala sich funktionale Programmiersprache nennen darf, wurde im Web kürz-lich erst ausführlich diskutiert. Odersky bezeichnet Scala schließlich in [Ode10a]als postfunktionale Sprache. Fest steht auf alle Fälle, dass Scala über eine Reihevon Features verfügt, die entweder der funktionalen Programmierung zuzurech-nen sind oder aus ihrem Umfeld entstammen.In Scala ist jede Funktion ein Wert8 und kann gleichberechtigt mit anderen Wer-ten behandelt werden. Das bedeutet beispielsweise, eine Funktion kann Argu-ment oder Ergebnis einer anderen Funktion9 sein, Funktionen können in Listengespeichert werden, und Funktionen können ineinander verschachtelt werden.Darüber hinaus unterstützt Scala auch Features wie Pattern Matching10 und Cur-rysierung11.Im Gegensatz zu vielen modernen und angesagten Programmiersprachen ist Sca-la statisch typisiert. Das heißt, der Typ aller Ausdrücke wird zur Kompilierzeitüberprüft und nicht erst zur Laufzeit, wie es bei dynamisch typisierten Sprachender Fall ist. Nachdem eine überwiegende Anzahl von Programmierfehlern Typ-fehler sind, ist die statische Typisierung unserer Ansicht nach im allgemeinen vor-zuziehen. Dem wesentlichen Nachteil, nämlich der Notwendigkeit, überall Typenangeben zu müssen, wird in Scala mit einem Typinferenzmechanismus begeg-net. Damit ist es an den meisten Stellen nicht notwendig Typen anzugeben. BeimÜbersetzen wird der Typ dann inferiert und überprüft, ob alles zusammenpasst.Scala hat ein sehr ausgefeiltes Typsystem, das neben generischen Klassen undpolymorphen Methoden auch Varianz-Annotationen, Upper und Lower Boundsund vieles mehr zur Verfügung stellt.Ein weiteres Merkmal von Scala, was übrigens für Scalable Language steht, istdie einfache Erweiterbarkeit. Damit ist Scala prädestiniert zur Erstellung von Do-main Specific Languages (DSLs). Zu guter Letzt kann Scala auch noch mit Unter-stützung für die .NET-Plattform aufwarten. Dabei handelt es sich allerdings nochnicht um einen Stand, der als „production ready” zu bezeichnen ist.

8 und damit ein Objekt9 Funktionen, die eine Funktion als Argument oder Ergebnis haben, heißen Higher Order Functions,zu deutsch: Funktionen höherer Ordnung.10 siehe Abschnitt 5.411 siehe Abschnitt 5.5

Page 16: Scala : objektfunktionale Programmierung

1.1 Was Führungskräfte über Scala wissen sollten 3

Scala auf der JVM ist seit Langem ausgewachsen und kann in allen Situationen,auch unternehmenskritisch, eingesetzt werden. Die Weiterentwicklung von Scalaist sehr aktiv, von Bugfix-Releases über neue Features wird uns in der nächstenZeit noch einiges erwarten. Für die kommenden Jahre steht der Fokus des Scala-Teams auf der noch besseren Unterstützung von Multicore-Architekturen.

1.1 Was Führungskräfte über Scala wissen sollten

Scala ist eine erwachsene, sehr durchdachte Programmiersprache. Mit Scala kön-nen Sie uneingeschränkt alles machen, was mit Java auch gemacht werden kann.Nachdem Scala in Bytecode für die JVM kompiliert wird, also in Java-Bytecode,können Sie den einmal erstellten Scala-Code natürlich auch aus Java heraus nut-zen. Zusammenfassend heißt das: Scala kann gefahrlos ausprobiert werden. Selbstwenn zu Java zurückgewechselt werden sollte, ist die Arbeit in Scala nicht um-sonst. Und die gesamte Werkzeugpalette, die für die Java-Entwicklung genutztwird, wie z.B. Eclipse, NetBeans oder Maven, wird zur Entwicklung von Scala-Code einfach weiter benutzt.Warum aber sollten Sie überhaupt zu Scala wechseln? Scala erhöht die Produkti-vität! Scala bietet eine Vielzahl von Features, mit denen Sie kürzeren und elegan-teren Code schreiben können. Sicherlich ist die bloße Anzahl von Lines of Codenicht sehr aussagekräftig, aber mit weniger Code gibt es mindestens statistischgesehen auch weniger Fehler. Scala ist strenger typisiert als Java. Auch dadurchschlupfen weniger Fehler durch. Und ein wesentlicher Vorteil beim Start mit Sca-la ist, dass es sich fast wie Java anfühlt und zusätzliche Features nach und nacheingeflochten werden können.Warum denn Scala und nicht eine der unzähligen anderen Sprachen? Dazu möch-ten wir nur einige Zitate wiedergeben:

„If I were to pick a language to use today other than Java, it would be Scala.”

James Gosling, Schöpfer von Java

„Scala, it must be stated, is the current heir apparent to the Java throne. No otherlanguage on the JVM seems as capable of being a “replacement for Java” as Sca-la, and the momentum behind Scala is now unquestionable. While Scala is not adynamic language, it has many of the characteristics of popular dynamic langua-ges, through its rich and flexible type system, its sparse and clean syntax, and itsmarriage of functional and object paradigms.”

Charles Nutter, Schöpfer von JRuby

„Though my tip though for the long term replacement of javac is Scala. I’m veryimpressed with it! I can honestly say if someone had shown me the Programming

Page 17: Scala : objektfunktionale Programmierung

4 1 Einführung

in Scala book by Martin Odersky, Lex Spoon & Bill Venners back in 2003 I’dprobably have never created Groovy.”

James Strachan, Schöpfer von Groovy

Wer nutzt Scala noch? Scala ist mittlerweile in einer Reihe von Firmen wie Sony,Siemens und Xerox angekommen. Betrachten wir beispielhaft zwei Erfolgsge-schichten:

1. Électricité de France Trading (EDFT) ist eine Tochterfirma von Frankreichsgrößter Energiefirma EDF, die sich mit dem Energiemarkt befasst. In den letz-ten Jahren hat EDFT einen substanziellen Teil der 300.000 Zeilen Java-Codefür „Trading and Pricing” erfolgreich durch Scala ersetzt. EDFT spricht von ei-ner signifikanten Steigerung der Produktivität und von sehr viel verbessertenSchnittstellen für ihre Händler. Der Teamleiter Alex McGuire hat mittlerwei-le EDFT verlassen und eine eigene Firma gegründet, die Scala-Consulting fürFinanzdienstleister und Handelsunternehmen bietet.

2. Twitter12 bietet einen sehr populären Realtime-Messaging-Service, den welt-weit über 70 Millionen User nutzen. Pro Tag verarbeitet die Twitter-Infrastruk-tur, die mittlerweile im Backend zum Großteil aus Scala-Code besteht, über 50Millionen Kurznachrichten, sogenannte Tweets.

Und was ist mit kommerziellem Support? Den gibt es bereits in verschiedensterForm. Es gibt eine sehr aktive Scala-Community und erste, auch deutsche, Firmen,die Scala-Consulting leisten. Ein wesentlicher Schritt war die Gründung der Fir-ma ScalaSolutions durch Martin Odersky selbst. Die Firma bietet Scala-Support,-Consulting und -Training an. Und nicht zuletzt damit können interessierte undgute Java-Programmierer innerhalb kürzester Zeit zu guten Scala-Entwicklern ge-schult werden.

1.2 Java-Scala-Integration

Auch wenn Sie Scala noch nicht kennengelernt haben, wollen wir an dieser Stellezur weiteren Motivation bereits etwas über die nahtlose Integration mit Java sa-gen. Aus Scala heraus können Sie Java-Klassen und -bibliotheken genauso nutzen,wie Sie das direkt in Java machen würden13.Umgekehrt geht das fast genauso einfach. Nur an ein paar kleinen Stellen müs-sen Sie ein bisschen mehr über die Interna wissen. Scala ist, was Sprachfeaturesangeht, mächtiger als Java. Alle Features werden aber durch Java-Bytecode reprä-sentiert. Daher kommen wir von Java aus überall heran, die Frage ist nur wie.

12 http://twitter.com/13 Eine kleine Besonderheit stellen Javas Wildcard und Raw Types dar. Auch dafür gibt es in Scala eineLösung, aber die würde an dieser Stelle etwas zu weit führen. Die Lösung, existenzielle Typen, werdenwir in Abschnitt 5.7.9 besprechen.

Page 18: Scala : objektfunktionale Programmierung

1.3 Über dieses Buch 5

In Scala ist im Gegensatz zu Java alles ein Objekt. Zur besseren Performanz wer-den die Objekte, die in Java als primitive Typen repräsentiert werden, wenn mög-lich in einen primitiven Wert umgewandelt, z.B. der Scala Int in den Java int.Ist dies nicht möglich, z.B. weil in Java primitive Datentypen nicht als Typpara-meter für generische Klassen zulässig sind, wird der Wert in die entsprechendeWrapper-Instanz umgewandelt. Beispielsweise wird der Scala Int in einer Listein eine Instanz der Klasse java.lang.Integer übersetzt.Scalas reine Objektorientierung lässt keine statischen Klassenmember zu. Statt-dessen hat Scala Singleton-Objekte. Aus einem Singleton-Objekt mit dem NamenMyObject wird eine Klasse mit dem Namen MyObject$ erzeugt, die das Objektüber das statische Feld MODULE$ nutzbar macht.Wenn es zum Singleton-Objekt keine dazugehörige Klasse gibt, es sich also umein sogenanntes Standalone-Objekt handelt, wird darüber hinaus noch eine Klas-se mit dem Namen MyObject erzeugt, die statische Member für alle Memberdes Scala-Objekts enthält. Damit wird dann auf das Member x des Scala-ObjektsMyObject in Scala wie auch in Java mit MyObject.x zugegriffen.Mit Scalas Traits ist es ein bisschen komplizierter, da Java kein entsprechendesKonstrukt kennt. Aus einem Trait wird immer ein Java-Interface und damit einTyp erzeugt. Über Variablen dieses Typs können alle Methoden der Scala-Objektegenutzt werden. Das Implementieren eines Traits in Java ist allerdings nicht prak-tikabel, außer der Trait enthält nur abstrakte Member.Javas Annotations und Exceptions werden von Scala unterstützt. Spezielle Scala-Annotations wie zum Beispiel @volatile, @transient und @serializablewerden in die entsprechenden Java-Konstrukte transformiert. Scala kennt kei-ne Checked Exceptions, bietet aber eine @throws-Annotation für die Java-Interoperabilität.

1.3 Über dieses Buch

Mit dem vorliegenden Buch wollen wir Sie in die faszinierende Welt von Sca-la entführen. Um Ihnen den Einstieg so angenehm wie möglich zu machen unddamit Sie das von uns Beschriebene gleich praktisch ausprobieren können, begin-nen wir in Kapitel 2 mit Informationen über die Scala-Tools und über die Toolszur Unterstützung des Entwicklungsprozesses.Anschließend befassen wir uns in drei Kapiteln mit der Programmiersprache Sca-la. In Kapitel 3 tasten wir uns an die Syntax heran und besprechen die imperati-ven Programmierkonzepte. Außerdem zeigen wir Ihnen, wie in Scala ausführbareScripts entwickelt werden, und erstellen ein erstes kompilierbares Programm.Auch wenn sich die Features von Scala nicht streng in Merkmale objektorientierterund funktionaler Programmiersprachen aufteilen lassen, haben wir eine solcheAufteilung in die beiden Kapitel 4 und 5 vorgenommen. Dabei findet sich alles,

Page 19: Scala : objektfunktionale Programmierung

6 1 Einführung

was eher dem Bereich Objektorientierung zuzuordnen ist, in Kapitel 4, und dieFeatures, die ursprünglich der funktionalen Programmierung entstammen bzw.enger mit ihr verbunden sind, in Kapitel 5.Die Popularität und Nutzbarkeit einer Programmiersprache lebt natürlich nichtnur vom Sprachkern. In zwei Kapiteln lenken wir unser Augenmerk auf die Bi-bliotheken, die die Scala-Distribution mitbringt. In Kapitel 6 geben wir zunächsteinen groben Überblick, bevor wir einige Bereiche wie das Collection-Frameworkund die hervorragende XML-Unterstützung von Scala ein wenig genauer betrach-ten. Der Actor-Bibliothek ist aufgrund der zunehmenden Bedeutung von neben-läufiger und Multicore-Programmierung ein eigenes Kapitel gewidmet, nämlichKapitel 7.Qualitativ hochwertige Software sollte gut dokumentiert und getestet sein. Die inder Scala-Entwicklung üblichen Ansätze stellen wir Ihnen in Kapitel 8 vor.Die letzten drei Kapitel geben Ihnen einen Einstieg in drei Scala-Frameworks. Inallen drei Kapiteln entwickeln wir jeweils eine kleine Beispielapplikation. Derin Kapitel 9 erstellte Talk Allocator ist eine Web-Applikation, die auf Lift14, ei-nem sehr umfangreichen Web-Framework, aufsetzt. Der Final-Grade-Calculatoraus Kapitel 10 nutzt das sehr leichtgewichtige Web-Framework Scalatra15. Undschließlich tauchen wir mit dem MovieStore in Kapitel 11 ein in die faszinierendeWelt von Actors und Software Transactional Memory mit Akka16. Über die vor-gestellten Frameworks hinaus gibt es eine Menge weiterer, sehr interessanter Fra-meworks wie beispielsweise ScalaModules, das bereits in [WBB10] beschriebenwurde.Auf der Website zum Buch http://scala.obraun.net/ finden Sie Links zumQuellcode zu einigen Kapiteln. Weitergehende Informationen zu Scala finden Sieauf der Scala-Website http://www.scala-lang.org/, auf vielen Mailinglis-ten, in Blogs etc. Eine Reihe englischsprachiger Scala-Bücher sind auch verfügbar,beispielhaft soll hier [OSV08] genannt werden.

1.4 Typographische und sonstige Konventionen

Den Shell-Kommandos wird ein $-Zeichen vorangestellt, das beim Abtippennicht mit eingegeben werden darf. Zeilen ohne das Dollarzeichen sind Ausgaben,zum Beispiel:

$ scala -versionScala code runner version 2.8.0.final -- Copyright

2002-2010, LAMP/EPFL

14 siehe [CBWD09] und http://liftweb.net/15 http://www.scalatra.org/16 http://akkasource.org/

Page 20: Scala : objektfunktionale Programmierung

1.4 Typographische und sonstige Konventionen 7

Kommandos in der interaktiven Scala-Umgebung wird als Prompt scala> vor-angestellt. Erstreckt sich die Eingabe über mehrere Zeilen, beginnen die folgen-den Zeilen mit fünf Leerzeichen, gefolgt von einem |-Symbol. Andere Zeilen sindAusgaben des Scala-Interpreters, beispielsweise:

scala> for (i <- 1 to 3)| println(i)

123

Zugunsten der besseren Lesbarkeit wird in diesem Buch darauf verzichtet, an je-der Stelle die weibliche und männliche Form anzugeben, wie z.B. „die Leserin/-der Leser” oder „der/die LeserIn”. Mit der dann genutzten, üblichen Form, z.B.„der Leser”, möchten wir selbstverständlich Leserinnen und Leser gleichermaßenansprechen.Und nun wünschen wir Ihnen viel Spaß beim Lesen und Ausprobieren.

Page 21: Scala : objektfunktionale Programmierung
Page 22: Scala : objektfunktionale Programmierung

Kapitel 2

Einrichten derArbeitsumgebung

Wenn Sie eine Fremdsprache erlernen wollen, werden Ihnen wohl die meistenMitmenschen empfehlen, möglichst viel darin zu sprechen. Analog verhält es sichmit dem Erlernen einer neuen Programmiersprache. Das Wichtigste ist, viel darinzu programmieren. Damit Ihnen das gut gelingt und Sie nicht zu sehr durch dieAuswahl und Einrichtung der Arbeitsumgebung vom schnellen Start in die Scala-Welt abgelenkt werden, möchten wir Ihnen in diesem Kapitel die Scala-Tools unddie Unterstützung für die gängigen Entwicklungsumgebungen vorstellen. In Ab-schnitt 2.1 erklären wir kurz die verschiedenen Kommandozeilenprogramme derScala-Distribution. Die für Scala üblichen Buildtools Maven und Sbt zeigen wirIhnen in Abschnitt 2.2. Abschließend werfen wir noch einen Blick auf die verfüg-bare IDE-Unterstützung in Abschnitt 2.3.

2.1 Die Scala-Shell und die Kommandozeile

Für die ersten Schritte in Scala reicht die Installation bzw. das Entpacken der Scala-Distribution. Diese finden Sie entweder auf http://www.scala-lang.org/oder, falls Sie unter Linux/*BSD/MacOS X/. . . arbeiten, mit hoher Wahrschein-lichkeit enthalten in Ihrem Paketverwaltungssystem.Installiert werden neben der Standardbibliothek im Wesentlichen die folgendenShell-Scripte bzw. Batch-Dateien (mit der Endung .bat):

scala – Der Scala-„Interpreter”.Mit dem scala-Kommando können Scala-Applikationen, analog zu Java-Applikationen, oder Scala-Scripte in der Java-Laufzeit-Umgebung gestartetwerden. Wird beim Aufruf kein Argument übergeben, startet die sogenannte

Page 23: Scala : objektfunktionale Programmierung

10 2 Einrichten der Arbeitsumgebung

Scala-Shell, ein interaktiver Kommandozeileninterpreter, also ein sogenannterREPL1, der im Folgenden noch genauer besprochen wird.

scalac – Der Scala-Compiler.Mit dem Scala-Compiler können JVM-1.5-kompatible.class-Files erzeugtwerden oder, falls -target:msil angegeben wird, MSIL Assembler-Codefür die .NET-Plattform2.

fsc – Der „Fast-Offline”-Compiler.Um nicht bei jedem Übersetzungsvorgang die JVM initialisieren und die not-wendigen Klassen laden zu müssen, startet fsc beim ersten Aufruf einenCompilation-Daemon, der für weitere Übersetzungen wiederverwendet werdenkann. Damit wird insbesondere das Kompilieren derselben Klassen extrembeschleunigt. Um inhaltliche Änderungen im genutzten Class-Path bei derKompilation zu berücksichtigen, muss der Daemon vorher explizit mit fsc-shutdown beendet werden. Der fsc wird auch genutzt, wenn ein Scala-Script mit dem scala-Kommando ausgeführt werden soll3.

scalap – Der Class-Datei-Dekodierer.Analog zu javap, dem Java-Class-Datei-Disassemblierer, dekodiert scalapdie mit dem Scala-Compiler erzeugten Class-Dateien. Bei der Verwendungvon javap bekommen Sie den disassemblierten Java-Code, mit scalap denScala-Code.

scaladoc – Der Dokumentations-Generator.Die Scala-Distribution enthält ein eigenes Tool, um aus den Quellcode-Dateiendie API als HTML-Dateien zu generieren. Obwohl die zu verwendenden Tagsfür die Dokumentation im Wesentlichen mit denen von Javadoc übereinstim-men, ist ein eigenes Tool notwendig, da ein Scala-API auch Objekte und Traitsenthalten kann. Darüber hinaus wurde scaladoc mit Scala 2.8 überarbeitetund besser an Scala angepasst.

sbaz – Das Scala-Bazar-System (SBaz)Das Scala-Bazar-System ist als Paketverwaltungssystem dazu gedacht eineScala-Installation zu pflegen. sbaz bietet Kommandos, um Pakete zu installie-ren, zu entfernen und auf eine neuere Version zu aktualisieren. Darüber hinauskönnen selbstentwickelte Pakete mithilfe von sbaz verpackt und verteilt wer-den. Mit dem auch in der Scala-Distribution enthaltenen sbaz-setup kannauf einfache Weise ein neues Verzeichnis mit Sbaz initialisiert werden.

Die Kommandos, die auf das JDK oder JRE zurückgreifen, scala, scalac undfsc, berücksichtigen die Umgebungsvariablen, falls diese gesetzt sind:

JAVACMD, das Java-Kommando um den Scala-Code auszuführen,

1 Read-Eval-Print-Loop2 Scala ist mit dem Fokus auf die JVM entwickelt worden. Die Unterstützung der .NET-Plattform ist einzusätzliches Feature, befindet sich noch in einem früheren Entwicklungsstadium und wird in diesemBuch daher nicht weiter betrachtet werden.3 Wird ein Scala-Script gestartet, so wird es automatisch übersetzt und der Bytecode ausgeführt.

Page 24: Scala : objektfunktionale Programmierung

2.1 Die Scala-Shell und die Kommandozeile 11

JAVA_HOME, das Verzeichnis, in dem die JDK/JRE-Programme installiert sind,und

JAVA_OPTS, Optionen, die an den JAVACMD übergeben werden.

Im Folgenden werden wir die verschiedenen Kommandozeilenprogramme nochetwas genauer betrachten. Es ist dabei nicht notwendig, dass Sie den gelistetenQuellcode, insbesondere den von Tools wie scalap generierten Quellcode, andieser Stelle bereits genau verstehen. Ziel ist vielmehr, einen Einblick zu gewin-nen, was mit den Kommandos alles möglich ist.

2.1.1 Der Scala-Interpreter

Obwohl Scala-Sourcecode grundsätzlich in Bytecode für die JVM oder für die.NET-Plattform übersetzt wird, bringt die Distribution zusätzlich einen interakti-ven Interpreter mit. Gestartet wird dieser mit dem Kommando scala ohne wei-tere Argumente.

Listing 2.1: Start des interaktiven Scala-Interpreters

$ scalaWelcome to Scala version 2.8.0.final (Java HotSpot(TM)

64-Bit Server VM, Java 1.6.0_20).Type in expressions to have them evaluated.Type :help for more information.

scala>

Nach dem Starten können in der Scala-Shell Ausdrücke eingegeben werden. Diesewerden unmittelbar nach dem Betätigen der Return-Taste ausgewertet und dasErgebnis wird ausgegeben.

Listing 2.2: Einfache Berechnungen in der Scala-Shell

scala> 1+2res0: Int = 3

scala> println("Hello World!")Hello World!

scala> println("Das Ergebnis lautet "+(1+2))Das Ergebnis lautet 3

scala> println("Das Ergebnis lautet "+res0)Das Ergebnis lautet 3

Listing 2.2 zeigt einige einfache Beispiele, die Sie in der Scala-Shell berechnenlassen können. Als Erstes werden die Integer 1 und 2 addiert und die Summeausgegeben. Das Ergebnis wird automatisch der Variable res0 zugewiesen und

Page 25: Scala : objektfunktionale Programmierung

12 2 Einrichten der Arbeitsumgebung

kann damit später wieder referenziert werden. Darüber hinaus wird der inferier-te Typ ausgegeben. Das nächste Ergebnis einer Berechnung wird dann der Va-riablen res1 zugewiesen usw. Die drei folgenden Eingaben berechnen kein Er-gebnis, sondern haben nur einen Nebeneffekt, der etwas auf der Kommandozeileausgibt.Selbstverständlich können auch eigene Variablen eingeführt werden, und es istmöglich, Scala-Code über mehrere Zeilen einzugeben (siehe Listing 2.3). Zusätzli-chen Eingabezeilen werden 5 Leerzeichen, gefolgt von einem |-Symbol, automa-tisch durch die Scala-Shell vorangestellt. Um eine zusätzliche Zeile zuzulassen,muss die Shell erkennen, dass es sich bei der Eingabe noch um keinen korrektenAusdruck handelt.

Listing 2.3: Eigene Variablen und mehrzeiliger Code

scala> val sum = 1 until 100 reduceLeft {(x,y) => x + y}sum: Int = 4950

scala> for (i <- 1 to 5)| if (i % 2 == 0)| println(i+" ist gerade und zwischen 1 und 5")

2 ist gerade und zwischen 1 und 54 ist gerade und zwischen 1 und 5

Wir werden später noch sehen, dass die Scala-Shell nicht ganz gleichwertig zumKompilieren und Ausführen ist4.Mit scala -i <dateiname> kann eine Scala-Datei beim Start in die Scala-Shell geladen werden. In der Shell-Sitzung selbst kann jederzeit mit dem Kom-mando :load <dateiname> eine Datei geladen und interpretiert werden, mit:jar <dateiname> wird ein JAR zum Classpath hinzugefügt. Seit Scala 2.8.0beherrscht die Shell die Autovervollständigung mittels der Tabulatortaste. Die Be-deutung einiger weiterer Kommandos kann mit :help abgefragt werden.Interessant, um die Mächtigkeit von Scala auch direkt auf der Kommandozeile zunutzen, ist das Flag -e, mit dem ein Scala-Ausdruck angegeben werden kann, derunmittelbar ausgewertet wird, wie z.B. in Listing 2.4 angegeben.

Listing 2.4: Ausgabe des absoluten Pfads auf der Kommandozeile mithilfe von Scala

$ scala -e ’val file = new java.io.File(".")println(file.getAbsolutePath())’

/home/obraun/projects/scalabuch/.

Wird dem scala-Interpreter ein Scala-Script, also eine Quellcode-Datei mit Scala-Code, als Argument übergeben, wird dieses mit dem fsc kompiliert und so-

4 Beispielsweise kann sie nicht mit der Besonderheit umgehen, dass Member einer Klasse die mitprivate modifiziert sind, im Companion-Objekt sichtbar sind und umgekehrt.

Page 26: Scala : objektfunktionale Programmierung

2.1 Die Scala-Shell und die Kommandozeile 13

fort ausgeführt. Wird der Kommandozeilenschalter -nocompdaemon genutzt, sowird mit scalac kompiliert. Um das wiederholte Ausführen eines unveränder-ten Scripts zu beschleunigen, kann -savecompiled angegeben werden.In einem Scala-Script kann alles stehen, was in der interaktiven Shell eingegebenwerden darf, d.h. es können Variablen, Funktionen, Klassen und Objekte defi-niert werden. Das Script wird Zeile für Zeile abgearbeitet und mit dem Erreichendes Dateiendes beendet. Ein Scala-Script kann auch selbst ausführbar gemachtwerden. Unter Unix funktioniert das mit dem Shebang, wie in Listing 2.5 darge-stellt. Der Header muss allerdings im Gegensatz zu sonstigen Shell-Scripten fürdas Scala-Script mit !# abgeschlossen werden.

Listing 2.5: Unter Unix ausführbares Scala-Script

#!/bin/shexec scala "$0" "$@"!#println("Hallo Leser!")

Unter Windows sieht das ausführbare Script wie in Listing 2.6 dargestellt aus.

Listing 2.6: Unter Windows ausführbares Scala-Script

::#!@echo offcall scala %0 %*goto :eof::!#println("Hallo Leser!")

Zu guter Letzt wird das Kommando scala <objectname> analog zu java<classname> genutzt, um ein vorher mit dem Scala-Compiler übersetztes Ob-jekt auszuführen. Das Objekt muss dazu eine main-Methode enthalten.

2.1.2 Die Scala-(De-)Compiler

Mit dem Scala-Compiler werden Scala-Quelltext-Dateien in Class-Dateien für dieJVM übersetzt bzw. mit -target:msilMSIL-Assembler-Code für .NET erzeugt.Für den normalen Gebrauch reicht es zu wissen, dass der Aufruf

$ scalac <filename>

bzw.

$ fsc <filename>

die in <filename> enthaltenen Objekte und Klassen übersetzt. Im Folgendenwollen wir für die daran Interessierten noch etwas tiefer einsteigen.

Page 27: Scala : objektfunktionale Programmierung

14 2 Einrichten der Arbeitsumgebung

Der Compiler durchläuft verschiedene Phasen wie z.B. parse, analyze unduncurry. Mit dem Kommandozeilenflag -Xprint: <phases> ist es möglich,die Datei nach der angegebenen Phase ausgeben zu lassen und damit etwas hin-ter die Kulissen zu blicken. Beispielsweise wird das in Listing 2.7 definierte Objektnach dem Parsen wie in Listing 2.8 dargestellt ausgegeben.

Listing 2.7: Hallo.scala

object Hallo extends Application {println("Hallo Leser!")

}

Listing 2.8: Hallo.scala nach dem Parsen

[[syntax trees at end of parser]]// Scala source: Hallo.scala

package <empty> {object Hallo extends Application with scala.ScalaObject

{def <init>() = {super.<init>();()

};println("Hallo Leser!")

}}

Mit dem Flag -print kann das Programm ohne alle Scala-spezifischen Featuresausgegeben werden (siehe Listing 2.9).

Listing 2.9: scalac -print Hallo.scala

[[syntax trees at end of cleanup]]// Scala source: Hallo.scala

package <empty> {final class Hallo extends java.lang.Object with

Application with ScalaObject {<stable> <accessor> def executionStart(): Long =

Hallo.this.executionStart;private[this] val executionStart: Long = _;<accessor> def scala$Application$_setter_

$executionStart_=(x$1: Long): Unit = Hallo.this.executionStart = x$1;

def main(args: Array[java.lang.String]): Unit = scala.Application$class.main(Hallo.this, args);

def this(): object Hallo = {Hallo.super.this();scala.Application$class./*Application$class*/$init$

(Hallo.this);scala.this.Predef.println("Hallo Leser!");

Page 28: Scala : objektfunktionale Programmierung

2.1 Die Scala-Shell und die Kommandozeile 15

()}

}}

Selbstverständlich können über diverse Flags die notwendigen Pfade wie Class-path oder Sourcepath angegeben werden. Flags wie -verbose, -optimise und-explaintypes und eine Vielzahl von advanced options bieten weitere Möglich-keiten, um den Übersetzungsvorgang an die eigenen Bedürfnisse anzupassen. Diejeweilige Bedeutung kann der in der Scala-Distribution mitgelieferten Dokumen-tation entnommen werden.Der Fast Offline Compiler fsc startet einen Kompilierungsdaemon, der an einemSocket weitere Übersetzungsvorgänge annimmt. Mit dem Flag -verbose lässtsich schön beobachten, was beim ersten bzw. beim wiederholten Aufruf passiert.Mit -reset können die vom compile server verwendeten Caches geleert werden.Mit -shutdown kann der Daemon beendet werden.Die übersetzten Class-Dateien können mit dem im JDK enthaltenen Komman-do javap disassembliert werden. Beispielsweise wurden aus der Scala-DateiHallo.scala (siehe Listing 2.7) die zwei JVM-Klassen Hallo und Hallo$ er-zeugt (siehe Listing 2.10)

Listing 2.10: javap Hallo und javap Hallo$

// Das ist Javapublic final class Hallo extends java.lang.Object{

public static final void main(java.lang.String[]);public static final void scala$Application$_setter_

$executionStart_$eq(long);public static final long executionStart();

}public final class Hallo$ extends java.lang.Object

implements scala.Application,scala.ScalaObject{public static final Hallo$ MODULE$;public static {};public void main(java.lang.String[]);public void scala$Application$_setter_$executionStart

_$eq(long);public long executionStart();

}

Wird in Scala entwickelt, so ist es natürlich sinnvoller, die Information als Scala-Code und nicht nur als Java-Code ausgeben zu können. Dafür bringt die Scala-Distribution das Kommando scalap, den scala class file decoder, mit. Da sich dasaktuelle Verzeichnis nicht automatisch im Classpath befindet, muss es mit -cp. hinzugefügt werden. Listing 2.11 zeigt den dekodierten Scala-Code der beidenDateien Hallo.class und Hallo$.class.

Page 29: Scala : objektfunktionale Programmierung

16 2 Einrichten der Arbeitsumgebung

Listing 2.11: scalap -cp . Hallo und scalap -cp . Hallo$

object Hallo extends java.lang.Object with scala.Application with scala.ScalaObject {

def this() = { /* compiled code */ }}package Hallo$;final class Hallo$ extends scala.AnyRef with scala.

ScalaObject with scala.Application {final val executionStart: scala.Long;def executionStart(): scala.Long;def scala$Application$_setter_$executionStart_=(scala.

Long): scala.Unit;def main(scala.Array[java.lang.String]): scala.Unit;def this(): scala.Unit;

}object Hallo$ {final val MODULE$: Hallo$;

}

2.1.3 Der Dokumentationsgenerator

Auch für das Generieren der API-Dokumentation gibt es mit scaladoc ein eige-nes Werkzeug. Sah die bisher mit scaladoc erzeugte Dokumentation der Java-API-Dokumentation noch sehr ähnlich, enthält die aktuelle Scala-Version 2.8 nunein neues Dokumentationswerkzeug. Die notwendige Dokumentation des Quell-codes sieht allerdings immer noch fast genauso wie die für Java aus und verwen-det auch dieselben Tags zum Auszeichnen der Information. Zusätzlich könnenMakros definiert und eine Wiki-Syntax genutzt werden. Aus dem Quellcode ausListing 2.12 wird mit scaladoc die in Abbildung 2.1 dargestellte HTML-Seiteerzeugt. Die Dokumentation mit scaladoc wird in Kapitel 8 noch ausführlicherbesprochen.

Listing 2.12: Hallo.scala mit Quellcode-Dokumentation für scaladoc

/*** Hallo-Leser-Applikation* @author Oliver Braun* @version 1.0.0*/object Hallo extends Application {println("Hallo Leser!")

}

Page 30: Scala : objektfunktionale Programmierung

2.2 Buildtools 17

Abbildung 2.1: API-Dokumentation des Hallo-Objektes

2.2 Buildtools

Zum Erstellen von Scala-Applikationen gibt es viele Möglichkeiten. Im Folgendenwollen wir zwei Wege vorstellen. Gerade Umsteiger von Java, die bereits mit Ma-ven gearbeitet haben, werden sofort mit dem Maven-Scala-Plugin loslegen kön-nen. Alle, die noch keine Erfahrung mit Maven haben, finden einen Einstieg inAbschnitt 2.2.1. Im darauf folgenden Abschnitt 2.2.2 stellen wir das Simple BuildTool (Sbt) vor. Sbt ist ein in Scala für Scala geschriebenes Tool, das durch Scala-Code konfiguriert und erweitert werden kann. Innerhalb der Scala-Communitygewinnt Sbt immer mehr an Bedeutung.

2.2.1 Das Maven-Scala-Plugin

Apache Maven ist ein Build- und Konfigurationsmanagementwerkzeug für dieSoftwareentwicklung, das im Java-Umfeld eine große Verbreitung hat. Mithilfe ei-nes sogenannten Project Object Model (POM) kann Maven das Erstellen der Softwa-re inklusive Abhängigkeiten, Dokumentation, Tests etc. steuern. Für mehr Infor-mationen über Maven besuchen Sie bitte die Website http://maven.apache.org/ oder nehmen Sie eines der Bücher über Maven zur Hand, wie z.B. [Son08].Über Plugins ist es möglich, Maven an neue Aufgabenfelder anzupassen. DasMaven-Scala-Plugin5 dient dazu, Scala-Quellcode mit Maven zu kompilieren, zutesten und ablaufen zu lassen sowie der Erzeugung der Dokumentation.Um mit Maven zu arbeiten, reicht es zunächst, Maven in einer möglichst aktuel-len Version zu installieren6. Maven selbst setzt wiederum eine vorhandene Java-Installation voraus. Eine Scala-Distribution muss zur Verwendung mit Maven we-

5 http://scala-tools.org/mvnsites/maven-scala-plugin/6 Die folgende Einführung in das Maven-Scala-Plugin nutzt Maven in der Version 2.2.1.

Page 31: Scala : objektfunktionale Programmierung

18 2 Einrichten der Arbeitsumgebung

der auf dem System vorhanden sein noch installiert werden. Wurde Maven erfolg-reich installiert, kann ein Maven-Projekt auf der Kommandozeile mit

$ mvn archetype:generate

angelegt werden. Das Kommando mvn startet Maven. Das Argument arche-type:generate ist ein sogenanntes Ziel (engl. Goal). Genau genommen stehtdas Präfix archetype für das Plugin, welches das Ziel generate enthält. WennSie Maven zum ersten Mal starten, wird eine Vielzahl von Daten in das lokaleMaven-Repository heruntergeladen. Unter Unix befindet es sich beispielsweiseunter ˜/.m2/repository/.Maven bietet eine Reihe von Archetypen an, deren Nummerierung sich immerdann ändern kann, wenn neue Archetypen aufgenommen werden. Für die Scala-Entwicklung gibt es scala-archetype-simple unter der derzeitigen Num-mer 272. Mit der Version 1.2 des Scala-Plugins, der GroupID, der ArtifactID undder Version lässt sich dann beispielsweise das Artefakt my-app in der Gruppeorg.obraun unter der Version 1.0 erstellen. Die von Maven erzeugte Verzeich-nisstruktur sieht im konkreten Beispiel folgendermaßen aus:

my-app|-- pom.xml+-- src

|-- main| +-- scala| +-- org| +-- obraun| +-- App.scala+-- test

+-- scala+-- org

+-- obraun|-- AppTest.scala+-- MySpec.scala

Die in der Datei App.scala erzeugte HelloWorld-Applikation kann dann ausdem Verzeichnis my-app mit

$ mvn compile

übersetzt werden. Die in den Dateien AppTest.scala und MySpec.scala ge-nerierten Tests können mit

$ mvn test

ausgeführt werden. Mit

$ mvn package

wird ein Java-Archiv (.jar-Datei) erzeugt, das mit

Page 32: Scala : objektfunktionale Programmierung

2.2 Buildtools 19

$ scala -cp target/my-app-1.0-SNAPSHOT.jar \org.obraun.scala.App

Hello World auf die Konsole schreibt.Die angegebenen Ziele, compile, test und package, sind Standard-Maven-Ziele, die das Scala-Plugin nutzen und gemäß der Maven-Spezifikation aufein-ander aufbauen.Das Scala-Plugin bringt aber noch einige spezielle Ziele mit. Um nur die Appli-kationsquellen zu übersetzen, kann mvn scala:compile genutzt werden. DasKommando mvn scala:testCompile übersetzt hingegen nur die Testquellen.Sehr interessant ist das Ziel scala:cc, mit dem alle Quellen sofort beim Spei-chern automatisch übersetzt werden. Wird das Ziel mit mvn scala:cc gestartet,läuft es solange, bis es explizit abgebrochen wird.Zum Erzeugen der Scala-Dokumentation dienen die Ziele site bzw. scala:doc.Über scala:run kann eine Applikation gestartet werden, über scala:consolestartet die interaktive Scala-Shell.Die Konfiguration von Maven erfolgt in der Datei pom.xml. Dort kann beispiels-weise die zu verwendende Scala-Version als Property angegeben werden. Wirdeine andere Version als die vordefinierte 2.7.0 angegeben, z.B. 2.8.0, muss dieScala-Distribution nicht installiert werden, sondern wird von Maven selbst her-untergeladen, im lokalen Repository installiert und gestartet.Eine Vielzahl weiterer Konfigurationen können in der Datei oder auch in mehre-ren hierarchisch aufeinander aufbauenden POMs vorgenommen werden. Um bei-spielsweise die HelloWorld-Applikation mit mvn scala:run ohne Angabe wei-terer Argumente auszuführen, kann ein Launcher angegeben werden, beispiels-weise:

<launcher><id>helloWorld</id><mainClass>org.obraun.scala.App</mainClass>

</launcher>

Werden mehrere Launcher definiert, wird der erste ausgewählt, außer die ID wirdals Argument verwendet, z.B. mvn scala:run helloWorld.Weil eine ausführliche Einführung in Maven den Rahmen dieses Buches deut-lich sprengen würde, beenden wir hiermit diesen Überblick. Weitere Informa-tionen über das Maven-Scala-Plugin gibt es unter http://scala-tools.org/mvnsites/maven-scala-plugin/.

2.2.2 Simple Build Tool

Das Simple Build Tool Sbt ist ein Buildtool speziell für Scala. Es ist als Jar-File sbt-launch.jar auf der Projekt-Homepage7 verfügbar. In der Online-

7 http://code.google.com/p/simple-build-tool/

Page 33: Scala : objektfunktionale Programmierung

20 2 Einrichten der Arbeitsumgebung

Dokumentation wird empfohlen, das Jar mit einer Heapsize von 512 MB auszu-führen und sich am besten ein Script zum Starten zu bauen. Unter Unix sieht dasempfohlene einzeilige Script wie folgt aus8:

java -Xmx512M -jar /path/to/sbt-launch.jar "$@"

Unter Windows wird empfohlen, folgende Batchdatei zu nutzen9:

set SCRIPT_DIR=%~dp0java -Xmx512M -jar "%SCRIPT_DIR%sbt-launch.jar" %*

Ein möglicherweise vorhandener Proxy-Server muss mit den in Java üblichen Pro-perties gesetzt werden10. Um Sbt in Ihren Projekten dann einfach nutzen zu kön-nen, sollte das Verzeichnis in dem es sich befindet im Pfad sein oder durch einenAlias gefunden werden11.Auch Sbt benötigt wie Maven nur eine vorhandene Java-Installation. Die notwen-dige Scala-Distribution wird von Sbt selbst installiert. Soll ein Projekt mit Sbt ver-waltet werden, muss zuerst ein Projektverzeichnis erstellt und in diesem dannsbt gestartet werden.

Listing 2.13: Erstellen eines Sbt-Projekts

$ mkdir my-app$ cd my-app$ sbtProject does not exist, create new project? (y/N/s) yName: my-appOrganization: org.obraunVersion [1.0]:Scala version [2.7.7]: 2.8.0sbt version [0.7.4]:

In Listing 2.13 wird zunächst ein Verzeichnis my-app erzeugt und dort hineingewechselt. Der Aufruf von sbt findet im aktuellen Verzeichnis kein existieren-des Sbt-Projekt und fragt daher, ob ein neues erzeugt werden soll. Wird ’s’ fürscratch eingegeben, wird kein komplettes Projekt erzeugt, sondern vorhandenerCode kann dann durch Eingabe von run sofort übersetzt und ausgeführt.Wird wie in Listing 2.13 ’y’ eingegeben, so fragt Sbt nach Name, Organisationund Version des Projektes sowie der zu verwendenden Scala- und Sbt-Version. Dievon Sbt angelegte Verzeichnisstruktur sieht unmittelbar danach folgendermaßenaus:

8 /path/to muss natürlich durch den richtigen Pfad ersetzt werden.9 Bei diesem Beispiel müssen Batchfile und sbt-launch.jar im selben Verzeichnis sein.10 Zum Beispiel: java -Dhttp.proxyHost=myproxy -Dhttp.proxyPort=8080-Dhttp.proxyUser=username -Dhttp.proxyPassword=mypassword ...11 Zum Beispiel in der ˜/.bashrc mit den Eintrag: alias sbt = "/path/to/sbt"

Page 34: Scala : objektfunktionale Programmierung

2.2 Buildtools 21

my-app|-- lib|-- project| |-- boot| +-- build.properties|-- src| |-- main| | |-- resources| | +-- scala| +-- test| |-- resources| +-- scala+-- target

In den Verzeichnissen src/main/scala und src/test/scala werden danndie Scala-Quellcode-Dateien für die Applikation bzw. für die Tests erstellt undbearbeitet. Sollen zusätzlich noch Java-Quellen Verwendung finden, so sind dieUnterverzeichnisse src/main/java bzw. src/test/java zu erzeugen und zunutzen. Damit hat Sbt dieselbe Struktur für die Quellen wie Maven. Dateien, diein das Haupt-Jar oder das Test-Jar integriert werden sollen, müssen sich im jewei-ligen Unterverzeichnis resources befinden.Um Abhängigkeiten zu integrieren, müssen die entsprechenden Jars in das lib-Verzeichnis oder in ein Unterverzeichnis von lib kopiert werden. Sbt kann aberauch so konfiguriert werden, dass Abhängigkeiten automatisch heruntergeladenund in ein zusätzliches Verzeichnis lib_managed kopiert werden.Das project-Verzeichnis enthält die Informationen für Sbt. Die Property-Dateibuild.properties kann direkt editiert werden und nimmt Änderungen auf,die interaktiv durchgeführt wurden. In einem unter project zu erzeugendenbuild-Verzeichnis kann das Projekt konfiguriert werden. Plugins werden im Ver-zeichnis project/plugins konfiguriert. Da Sbt in Scala geschrieben ist, erfolgtdie Konfiguration auch unmittelbar in Scala. Das boot-Verzeichnis enthält diefür das Projekt benötigten Sbt- und Scala-Versionen. Wird ein Projekt übersetzt,landen alle generierten Dateien wie Class-Files und Dokumentation im target-Verzeichnis.Wird sbt ohne Argumente in der Shell eingegeben, startet Sbt im interaktivenModus. Werden auf der Kommandozeile Aktionen und Kommandos als Argu-mente übergeben, werden diese im Batch-Modus ausgeführt.Sbt kennt eine Vielzahl von Build Actions, z.B. clean, compile und run, undBuild Commands, z.B. exit und help. Interessant sind insbesondere die Kom-mandos der Form ˜ <command> für das automatische Ausführen des Komman-dos <command>, sobald sich eine Quelldatei geändert hat, und ++ <version><command> für das Wechseln auf eine andere Scala-Version. Das heißt beispiels-weise, mit ˜ compile wird jede geänderte Quellcode-Datei sofort übersetzt, und

Page 35: Scala : objektfunktionale Programmierung

22 2 Einrichten der Arbeitsumgebung

mit ++2.8.0.RC2 console kann eine Scala-Shell in der Version 2.8.0.RC2gestartet werden.Die Möglichkeiten von Sbt sind sehr umfangreich, und es kann perfekt in Scalaan die eigenen Bedürfnisse angepasst werden. Darüber hinaus gibt es bereits eineVielzahl von Erweiterungen in Form von Plugins wie z.B. SbtEclipsify, um ein Sbt-Projekt mit einer .classpath- und einer .project-Datei auszustatten, damites reibungslos in Eclipse bearbeitet werden kann. Eine ausführliche Darstellungvon Sbt finden Sie auf http://code.google.com/p/simple-build-tool/.

2.3 IDE-Support

Für die gängigen Entwicklungsumgebungen im Java-Umfeld gibt es eine Scala-Unterstützung in Form von Plugins. Mit Scala in der Version 2.8 ist eine Mengedes für IDE-Unterstützung notwendigen Codes in die Scala-Distribution selbstgewandert. Darauf aufbauend gibt es die Scala-IDE für Eclipse (siehe Abschnitt2.3.1), ein Plugin für NetBeans (siehe Abschnitt 2.3.2) und eines für IntelliJ IDEA(siehe Abschnitt 2.3.3). Selbstverständlich gibt es auch Unterstützung für Text-Editoren wie VIM, Emacs oder TextMate. Da es denjenigen, die einen Text-Editornutzen, nicht schwerfallen wird, den Scala-Support zu aktivieren, und die Einfüh-rung für alle, die die Editoren nicht kennen oder nutzen, zu umfangreich werdenwürde, verzichten wir auf eine Besprechung im Rahmen dieses Buches einfachganz.

2.3.1 Eclipse

Unter http://www.scala-ide.org/ finden Sie die Scala IDE for Eclipse12.Voraussetzung zum Installieren des Plugins ist Eclipse und das JDK in Version1.6. Die Scala IDE bietet Scala-Unterstützung für das Editieren, das Debuggingund die Navigation in Scala-Projekten. Außerdem können gemischte Scala/Java-Projekte bearbeitet werden. Ein Scala-Refactoring-Framework13, ein Quellcode-Formatierer14 und viele andere sinnvolle und wichtige Dinge wie XML-Syntax-Highlighting, Code Templates und Quick-Fix-Importe sind mittlerweile auch in-tegriert worden.Um die Scala-IDE für Eclipse zu installieren, öffnen Sie Eclipse und wählen imMenü Help den Punkt Install New Software. Als Site geben Sie die ge-wünschte URL an, die Sie unter http://download.scala-ide.org/ aus-wählen können15. Anschließend wählen Sie sowohl JDT Weaving for Scalaals auch Scala IDE for Eclipse aus und installieren es. Nach einem Neu-

12 http://eclipse.org/, Bücher über Eclipse gibt es mittlerweile auch einige wie z.B. [Ass05].13 http://scala-refactoring.org/14 http://scala-ide.assembla.com/wiki/show/scalariform15 z.B. http://download.scala-ide.org/nightly-update-helios-2.8.0.final

Page 36: Scala : objektfunktionale Programmierung

2.3 IDE-Support 23

Abbildung 2.2: Scala-Entwicklung mit der Scala-IDE für Eclipse

start von Eclipse stehen Ihnen eine Scala-Perspektive und die Scala-Wizards zurVerfügung. In Abbildung 2.2 ist ein Screenshot der Scala-IDE für Eclipse zu sehen.

2.3.2 NetBeans

Der NetBeans16-Support wird vom ErlyBird-Projekt17 bereitgestellt und befindetsich derzeit noch im Beta-Stadium. Um die Scala-Plugins zu installieren, laden Siedas Zip-File von der ErlyBird-Site herunter18 und entpacken es an einer beliebigenStelle. Anschließend öffnen Sie NetBeans und wählen im „Tools”-Menü den Ein-trag „Plugins”. Auf dem sich öffnenden Fenster können Sie im Reiter „Downloa-ded” den Button „Add Plugins” drücken. Im File-Dialog navigieren Sie dorthin,wo Sie die Plugins entpackt haben, und wählen alle .nbm-Dateien aus.Die Namen der dann angezeigten Plugins lassen schon auf den Umfang des Scala-Supports schließen: Scala Core, Scala Debugger, Scala Refactoring, um nur einigezu nennen. Klicken Sie dann auf „Install” und folgend Sie den Anweisungen, umdie Plugins zu installieren. Um unter NetBeans dann tatsächlich Scala entwickelnzu können, müssen Sie nur noch die $SCALA_HOME-Variable so setzen, dass derScala-Compiler etc. unter $SCALA_HOME/bin gefunden werden kann. Einen Ein-druck vom Arbeiten mit NetBeans soll Abbildung 2.3 vermitteln.

16 siehe [Bö08] und http://netbeans.org/17 http://erlybird.sourceforge.net/18 Mit Scala 2.8.0-final und NetBeans 6.9 zum Beispiel http://sourceforge.net/projects/erlybird/files/nb-scala/6.9v1.1.0/nb-scala-6.9v1.1.0.zip/download

Page 37: Scala : objektfunktionale Programmierung

24 2 Einrichten der Arbeitsumgebung

Abbildung 2.3: Scala-Entwicklung mit NetBeans und Scala-Plugins

2.3.3 IntelliJ IDEA

Die dritte Java-IDE, die mit Scala-Support aufwarten kann, ist die IntelliJ IDEA19.Die Community Edition ist frei und kann kostenlos heruntergeladen werden.Nach dem Starten kann über den Plugin-Manager das Scala-Plugin ausgewähltund installiert werden. Enthalten im Scala-Plugin sind die Unterstützung für Edi-tieren und Formatieren, Kompilieren, Testen und Debuggen, Refactoring, Navi-gation und Suche sowie diverse Intention Actions.Nach einem Neustart können Sie mit dem Scala-Entwickeln beginnen20. Ein neuesProjekt erzeugen Sie als Java-Projekt und wählen unter „Desired Technologies”Scala aus. Einen Blick auf die IntelliJ IDEA mit installiertem Scala-Plugin zeigtAbbildung 2.4.

19 http://www.jetbrains.com/idea/20 Die Variable JDK_HOME muss entsprechend gesetzt sein, z.B. unter Unix auf/usr/lib/jvm/sun-jdk-1.6.

Page 38: Scala : objektfunktionale Programmierung

2.3 IDE-Support 25

Abbildung 2.4: Scala-Entwicklung mit IntelliJ IDEA und Scala-Plugin

Page 39: Scala : objektfunktionale Programmierung
Page 40: Scala : objektfunktionale Programmierung

Kapitel 3

Grundlagen

Nachdem wir die Tools kennengelernt haben, wollen wir nun endlich mit denersten Schritten in Scala beginnen. Dazu werden wir als Erstes in Abschnitt 3.1die Syntax von Scala betrachten. Abschnitt 3.2 zeigt, wie in Scala imperativ, d.h.mit einfachen Kontrollstrukturen programmiert wird.Um Scala möglichst schnell und ohne große Hürden kennenzulernen, empfehlenwir Ihnen, die Beispiele beim Lesen gleich auszuprobieren. Die in Abschnitt 3.1angegebenen Code-Schnipsel können Sie direkt in die Scala-Shell eintippen. InAbschnitt 3.2 zeigen wir Ihnen Code-Stücke, die Sie in einer Datei speichern undmit scala <filename> als Script ausführen können.In Abschnitt 3.3 zeigen wir Ihnen dann, wie Sie ein Programm schreiben, dasmit dem Scala-Compiler in Bytecode übersetzt wird und anschließend ausgeführtwerden kann. Das Kapitel schließt mit einem kurzen Blick auf Annotations in Ab-schnitt 3.4.

3.1 Ein kleines bisschen Syntax

Die Scala-Syntax lehnt sich größtenteils an die Java-Syntax an. In einigen Berei-chen weicht sie aber deutlich ab. Ein wesentliches Ziel von Scala in Bezug aufdie Syntax ist, möglichst kurz und prägnant programmieren zu können. Deshalbsind einige Dinge wie beispielsweise das Semikolon am Ende jeder Zeile in Scalaweitestgehend optional.Scala ist statisch typisiert. Es sind aber im Gegensatz zu Java nicht notwendigüberall im Code Typen anzugeben. Scala kann mit einem Typinferenzmechanis-mus1 an den meisten Stellen den Typ selbst ermitteln2. Aufgrund der Tatsache,

1 siehe auch [CGLO06]2 Die Stellen, bei denen der Typ nicht inferiert werden kann, werden später explizit erwähnt.

Page 41: Scala : objektfunktionale Programmierung

28 3 Grundlagen

dass der Typ beispielsweise beim Definieren einer Variablen weggelassen werdenkann, ist es nicht möglich, die in Java und sonstigen C-ähnlichen Sprachen üb-liche Syntax <typ> <varname> beizubehalten. Stattdessen wird in Scala erstder Bezeichner angegeben, gefolgt von einem Doppelpunkt und dem Typ, z.B.

val age: Int

Kann der Typ inferiert werden und daher entfallen, muss auch der Doppelpunktweggelassen werden, z.B.

var sum = 0

Allerdings muss eine Variablendefinition in Scala immer mit var oder val be-ginnen und eine Initialisierung der Variable beinhalten, außer als Member einerKlasse oder eines Traits (siehe Kapitel 4), d.h. die oben angegebene Variable agemuss im Kontext einer Klasse oder eines Traits deklariert werden. Eine mit varerzeugte Variable ist veränderbar. Die mit val (für value) definierte Variable istunveränderbar und entspricht damit einer final-Variable in Java. Grundsätzlichsollte die unveränderliche Variante vorgezogen werden, was einem funktionalenProgrammierstil entspricht. Damit machen wir uns das Leben als Programmiereroftmals leichter. Beispielsweise muss nur einmal überprüft werden ob der zuge-wiesene Wert korrekt ist und wir können z.B. auch sicher sein, dass sie von keinemanderen Thread verändert werden kann.Bei parametrisierten Typen steht der Typparameter in eckigen Klammern, z.B.definiert

var list: List[Int] = List()

eine (veränderbare) Variable list vom Typ „Liste von Integer”3. Aufgrund derTypinferenz kann die rechte Seite der Definition ohne den Typparameter geschrie-ben werden. Selbstverständlich ist es aber auch möglich, ihn anzugeben:

val list: List[Int] = List[Int]()

Wird keine leere Liste, sondern die Liste mit den Elementen 1, 2 und 3 erzeugt,so kann auch der Typ der Variablen inferiert werden und muss nicht angegebenwerden:

val list = List(1,2,3)

In obigen Code-Schnipseln fällt auf, dass Listen-Objekte erzeugt werden, aber nir-gends ein new zu sehen ist, obwohl auch in Scala Objekte mit new erzeugt wer-den. Die verkürzte Schreibweise ist keine Besonderheit der Listen, sondern kanngrundsätzlich überall angewendet werden, wenn die Methode apply definiertist4. Es gilt nämlich, dass List() automatisch zu List.apply() expandiert

3 In Scala wird nicht zwischen primitiven und Referenztypen unterschieden, daher gibt es in Scalaeine Liste von Int.4 Die Methode applymuss im sogenannten Companion-Objekt der Klasse definiert werden. Für mehrInformation siehe Abschnitt 4.1.

Page 42: Scala : objektfunktionale Programmierung

3.1 Ein kleines bisschen Syntax 29

wird. Die Methode apply erzeugt dann ein neues leeres Listen-Objekt und gibtdies zurück. Analog wird List(1,2,3) zu List.apply(1,2,3) und erzeugtein Listen-Objekt, das die Zahlen 1, 2 und 3 enthält. Übrigens darf an dieser Stellenicht new List() geschrieben werden, da die Klasse List wie auch in Java ab-strakt ist. Das Nutzen der Factory-Methode apply erspart uns darüber hinaus nochdie Wahl einer bestimmten Implementierung. Wollen wir jedoch eine spezielle Im-plementierung nutzen, z.B. eine Java-ArrayList, so kann dies selbstverständlichauch gemacht werden:

val list = new java.util.ArrayList[Int]()

Die obige Code-Zeile zeigt, wie reibungslos sich Java-Code in Scala integrierenlässt. Dort wird nämlich eine Java-Liste genommen, die Scala-Integer enthält, diewiederum durch Nutzung des Java-Datentyps java.lang.Integer implemen-tiert sind. Wird zuvor sogar noch java.util.ArrayList mit

import java.util.ArrayList

importiert, reicht zu schreiben:

val list = new ArrayList[Int]()

Im Fall der Java-ArrayList kann die Variable list allerdings nicht mehr denTyp List[Int] haben, da sie keine Implementierung der Scala-Klasse List ist.Eine sinnvolle Typisierung wäre java.util.List[Int], der Typinferenzme-chanismus berechnet ohne Typangabe den Typ java.util.ArrayList[Int].Arrays erfahren in Scala keine spezielle Behandlung. Mit

val array = Array(1,2,3)

wird eine unveränderliche Variable array definiert, die ein Array der Länge 3mit den Werten 1, 2 und 3, das mittels Array.apply(1,2,3) erzeugt wurde,referenziert. Wird ein Array mit new erzeugt, entspricht der Parameter der Längedes Arrays, z.B. wird ein Array der Länge 5 mit

val array = new Array[Int](5)

erzeugt. Bei dieser Definition ist der Parameter-Typ Int zwingend anzugeben,weil sonst keine Information zur Verfügung steht, aus der der Typ hergeleitet wer-den könnte. Durch die Definition als val ist wie bei einer final-Variable in Javanur die Referenz auf das Array unveränderlich. Das Array selbst kann beispiels-weise mit

array(1) = 6

so verändert werden, dass es anschließend an der zweiten Position (der Indexfängt bei 0 an) den Wert 6 enthält. Der Index wird in Scala in runden Klammerngeschrieben. Auch diese einfache Möglichkeit, den Wert an der Stelle zu verän-

Page 43: Scala : objektfunktionale Programmierung

30 3 Grundlagen

dern, lässt sich auf andere Klassen übertragen. Das Code-Fragment array(1) =6 wird zu einem normalen Methodenaufruf expandiert, nämlich zu

array.update(1,6)

Dies funktioniert für alle Klassen, die die Methode update implementieren. Mitder zuvor vorgestellten Scala-Liste funktioniert dies allerdings nicht. Das liegtdaran, dass die mittels List() erzeugte Liste selbst unveränderbar (immutable)ist. Wird beispielsweise am Beginn der Liste mit dem Cons-Operator :: ein Wert„hinzugefügt”, wird eine neue Liste, bestehend aus dem Wert und der alten Lis-te, erzeugt. Die alte Liste bleibt unverändert.5 Die grundsätzliche Verwendungvon immutable data kommt aus der funktionalen Programmierung und stellt auchin Scala den bevorzugten Lösungsweg dar. Im Gegensatz zu vielen funktionalenProgrammiersprachen bleibt in Scala aber der imperative Weg über die Verände-rung von Daten auch weiterhin möglich, zum Beispiel weil er in manchen Fälleneffizienter ist.Wird zum Beispiel eine Liste aus vielen Elementen nach und nach zusammenge-setzt, kann stattdessen ein ListBuffer genutzt werden, der zum Schluss in eineListe umgewandelt wird.6 Listing 3.1 zeigt ein Beispiel.

Listing 3.1: Verwendung eines ListBuffers

import scala.collection.mutable.ListBufferval listbuffer = ListBuffer[Int]()listbuffer += 1listbuffer ++= List(2,3,4)val list = listbuffer.toList

Zunächst wird in der ersten Zeile der ListBuffer importiert, um in der zweitenZeile abkürzend ListBuffer schreiben zu können.7 In der zweiten Zeile wirdein leerer ListBuffer über die apply-Methode erzeugt. In der dritten Zeilewird die Zahl 1 angehängt, in der vierten Zeile die Zahlen 2, 3 und 4. Schließ-lich wird aus dem ListBuffer in der letzten Zeile die Liste List(1,2,3,4)erzeugt.Interessant in Bezug auf die Syntax sind insbesondere die Zeilen 3 und 4. Wer an-dere C-ähnliche Sprachen kennt, würde vermuten, listbuffer += 1 sei die ab-kürzende Schreibweise für listbuffer = listbuffer + 1. In Scala ist diesnicht immer der Fall. Die Zeichenkette += ist ein gültiger Bezeichner für eine Me-thode, die in der Klasse ListBuffer definiert ist. Nur wenn keine solche Metho-de vorhanden ist, wird der Operator wie in C-ähnlichen Sprachen interpretiert.Dies gilt für jeden Operator, der mit einem = endet. Ausnahmen sind die Ver-

5 In Java sind Strings immutable. "Hello"+"World" erzeugt aus den beiden Strings einen neuen.6 Analog wird in solchen Fällen in Java meist ein StringBuilder genutzt und in einen Stringumgewandelt.7 Warum scala.collection.immutable.List nicht importiert werden muss, liegt an dem soge-nannten Predef-Objekt, das in Kapitel 6 besprochen wird.

Page 44: Scala : objektfunktionale Programmierung

3.1 Ein kleines bisschen Syntax 31

gleichsoperatoren <=, >=, != sowie alle Operatoren, die zusätzlich mit einem =beginnen. Übrigens ist eine spezielle Syntax wie i++ zur Abkürzung von i = i+ 1 in Scala nicht erlaubt.In Scala steht listbuffer += 1 für listbuffer.+=(1), d.h. eine Methodemit dem Namen += und dem Argument 1. Dass dies möglich ist, liegt an zweiDingen: Erstens können in Scala Methoden nahezu beliebige Namen (siehe weiterunten) haben, und zweitens können in Scala Methodenaufrufe mit einem Argu-ment ohne Punkt und Klammern, also in Infix-Operator-Schreibweise, geschriebenwerden. Damit ist auch klar, was das zunächst etwas ungewöhnliche ++= in Zeile4 bedeutet: listbuffer.++=(List(2,3,4)).Beim genauen Hinsehen mutet sogar Zeile 5 noch etwas seltsam an. Sind wir esdoch aus anderen Sprachen gewöhnt, dass Methodennamen von Argumentlistengefolgt werden, d.h. wir hätten wohl eher listbuffer.toList() mit abschlie-ßendem leeren Klammerpaar erwartet. Weshalb diese möglich bzw. die Verwen-dung der Klammern hier gar nicht erlaubt ist, wird in Kapitel 4 näher beleuchtet.Nachdem Sie nun wissen, dass Operatoren eigentlich ganz normale Methodensind, betrachten wir nun das Code-Fragment:

Listing 3.2: Verwendung des Cons-Operators

1 :: List(2,3,4)

das die Liste List(1,2,3,4) erzeugt. Nach den bisherigen Regeln würdenwir nun vermuten, explizit (1).::(List(2,3,4))8 schreiben zu können. Dieswürde aber bedeuten, dass die Klasse Int (und alle anderen Klassen, deren Ob-jekte wir in Listen speichern können) die Methode :: implementieren müsste.Nachdem dies nicht nur unzweckmäßig, sondern auch nicht handhabbar ist undder Idee der Objektorientierung widerspricht, nach der eine Listenklasse für dieErzeugung einer Liste zuständig ist, ist es natürlich auch nicht so. Die Metho-de :: ist in der Klasse List implementiert, d.h. die obige Zeile heißt eigentlichList(2,3,4).::(1).Der Code in Listing 3.2 ist aber dennoch richtig. Die Regel dazu ist einfach: Je-der Operator, der mit einem Doppelpunkt endet, wird als Methode des rechtenOperanden interpretiert. Der linke Operand ist das Argument. Für alle anderenOperatoren (also die, die nicht mit : enden) gilt es umgekehrt: Die Methode wirdbeim linken Operanden gesucht, und der rechte Operand ist das Argument. Daes also nur abhängig vom Doppelpunkt am Ende ist, müssen Sie auch bei selbstdefinierten Methoden daran denken.Ganz beliebige Bezeichner sind in Scala aber auch nicht erlaubt. Für die Bildungvon zulässigen Bezeichnern gibt es drei Möglichkeiten:

8 Achtung: 1.::(List(2,3,4)) funktioniert, denn es bedeutet 1.0 :: List(1,2,3) und gibtals Ergebnis die Liste List(1.0, 2, 3, 4).

Page 45: Scala : objektfunktionale Programmierung

32 3 Grundlagen

1. Das erste Zeichen ist ein Buchstabe. Dann können darauf beliebige Buchstabenund Zahlen folgen. Nach einem Unterstrich _ können entweder wieder Buch-staben und Zahlen oder Operatorzeichen folgen. Beispiele: sum, unary_-

2. Das erste Zeichen ist ein Operatorzeichen. Dann kann eine beliebige Folge vonOperatorzeichen folgen. Beispiele: ::, ++=

3. Ein beliebige Zeichenkette zwischen Backquotes. Beispiele: ‘def‘, ‘class‘

Für den gesamten Quellcode (also auch für die Bezeichner) ist Unicode zulässig.Zulässige Operatorzeichen sind (geordnet nach aufsteigendem ASCII-Wert):

! # % & * + - / : < = > ? @ \ ^ | ~

Da in Scala Infix-Operatoren selbst definiert werden können, wird auch definiert,welcher Operator stärker bindet, ob also ein Ausdruck der Form:

expr1 <op1> expr2 <op2> expr3

mit drei Ausdrücken verknüpft durch zwei Operatoren als:

(expr1 <op1> expr2) <op2> expr3

oder:

expr1 <op1> (expr2 <op2> expr3)

zu interpretieren ist. Anders gefragt: Ob <op1> stärker bindet als <op2> oderumgekehrt.Scala folgt einer einfachen Regel: Je weiter unten das Zeichen, mit dem der Ope-rator beginnt, in Tabelle 3.1 steht, desto stärker bindet er. Zeichen in der gleichenZeile haben die gleiche Bindungsstärke. Damit folgt Scala dem, was auch in Javaüblich ist.

Tabelle 3.1: Operator-Bindung

(alle Buchstaben)|^&< >= !:+ -* / %(alle anderen Sonderzeichen)

Handelt es sich bei <op1> und <op2> um denselben Operator oder habensie die gleiche Bindungsstärke, entscheidet die Assoziativität darüber, was zu-erst berechnet wird. Es gilt: Die Operatoren, die mit einem : enden, sind rechts-assoziativ, alle anderen sind links-assoziativ. Das heißt zum Beispiel 1 :: 2 ::

Page 46: Scala : objektfunktionale Programmierung

3.1 Ein kleines bisschen Syntax 33

List() bedeutet 1 :: (2 :: List()), und 1 + 2 + 3 steht für (1 + 2)+ 3.In Scala werden folgende Schlüsselwörter verwendet, die nicht bzw. nur unterVerwendung von Backquotes als Bezeichner genutzt werden können:

abstract case catch class defdo else extends false finalfinally for forSome if implicitimport lazy match new nullobject override package private protectedrequires return sealed super thisthrow trait try true typeval var while with yield_ : = => <- <: <% >: # @

Darüber hinaus sind die Unicode-Zeichen⇒ (\u21D2) und← (\u2190) reserviert.Neben den auch in Java üblichen Literalen für Zahlen, Character, Boolean undStrings gibt es in Scala auch die Möglichkeit, mehrzeilige Zeichenketten anzuge-ben, z.B.

"""Dies isteine mehrzeiligeZeichenkette"""

Diese Zeichenkette enthält allerdings die Leerzeichen vor eine und Zeichen-kette. Um diese automatisch zu entfernen, kann die Methode stripMargingenutzt werden, z.B.

"""Dies ist|eine mehrzeilige|Zeichenkette""".stripMargin

Kommentare in Scala werden entweder mit // begonnen und enden mit dem Zei-lenende oder mit /* und enden mit */. Im Gegensatz zu Java können mehrzeiligeKommentare verschachtelt werden. Das führt allerdings dazu, dass ein Kommen-tar der Form

/* Dies ist ein mit zwei "/*" begonnener, aber nichtkorrekt beendeter Kommentar */

nicht zulässig ist.Scala bietet eine spezielle Syntax für Tupel. Tupel sehen aus wie Parameterlisten.Sie beginnen und enden mit einer öffnenden bzw. schließenden runden Klammerund können mehrere durch Kommata getrennte Komponenten haben, z.B.

val myTuple = ("Hello", 1, true, 5.7)

Die Komponenten können verschiedene Typen haben. Der Typ wird dann analogzum Tupel geschrieben, d.h. das Tupel myTuple hat den Typ (String, Int,Boolean, Double). Auf die einzelnen Komponenten können wir mit den Me-

Page 47: Scala : objektfunktionale Programmierung

34 3 Grundlagen

thoden _<positionsnummer> zugreifen, wobei die Positionsnummer bei 1 be-ginnt. Es ergibt sich also in der interaktiven Scala-Shell für myTuple:

scala> val first = myTuple._1first: java.lang.String = Hello

scala> val second = myTuple._2second: Int = 1

scala> val third = myTuple._3third: Boolean = true

scala> val fourth = myTuple._4fourth: Double = 5.7

Tupel können auch auf der linken Seite einer Zuweisung genutzt werden, umdie verschiedenen Komponenten eines Tupels verschiedenen Bezeichnern zuzu-ordnen. Beispielsweise können wir statt der vier einzelnen Zuweisungen nur eineeinzelne Zuweisung nutzen, die gleich alle Komponenten auf einmal den entspre-chenden Bezeichnern zuweist:

scala> val (first, second, third, fourth) = myTuplefirst: java.lang.String = Hellosecond: Int = 1third: Boolean = truefourth: Double = 5.7

Scala hat noch eine weitere Möglichkeit, mehreren Bezeichnern gleichzeitig einenWert zuzuweisen. Es ist möglich, mehrere Bezeichner links vom Zuweisungsope-rator mit Komma getrennt zu schreiben und auf der rechten Seite einen Ausdruckanzugeben, z.B.

scala> val i, j = 12i: Int = 12j: Int = 12

Nutzen wir einen Wert eines veränderlichen Datentyps auf der rechten Seite, be-kommt jede Variablen ihren eigenen Wert. In der folgenden Sitzung weisen wirzunächst m und n eine veränderliche, leere Menge zu. Anschließend fügen wir ei-ne 12 in die Menge m ein und geben beide Mengen aus. Wie in der Ausgabe zusehen ist, bleibt die Menge n unverändert:

scala> val m, n = scala.collection.mutable.Set[Int]()m: scala.collection.mutable.Set[Int] = Set()n: scala.collection.mutable.Set[Int] = Set()

scala> m += 12res0: m.type = Set(12)

scala> println("m = "+m+"\nn = "+n)m = Set(12)n = Set()

Page 48: Scala : objektfunktionale Programmierung

3.1 Ein kleines bisschen Syntax 35

In diesem Fall kann m += 12 übrigens gar keine Abkürzung für m = m + 12sein, denn m ist ja ein val. Der Versuch, die vermeintlich ausführliche Schreibwei-se zu nutzen, führt zu einer Fehlermeldung:

scala> m = m + 12<console>:6: error: reassignment to val

m = m + 12^

Das heißt, in diesem Fall entspricht m += 12 dem Methodenaufruf m.+=(12),der das von m referenzierte Set verändert.Analog zur Variablendefinition mit var oder val beginnt eine Funktionsdefiniti-on immer mit dem Schlüsselwort def. Ihm folgen der Funktionsname und die Pa-rameterliste9. Der Rückgabetyp wird, sofern er angegeben werden soll oder muss,mit einem Doppelpunkt getrennt nach den Argumenten angegeben, z.B.

def hello(name: String) {println("Hello "+name)

}

Der in Scala implementierte lokale Typinferenzmechanismus kann die Parameter-typen nicht berechnen. Daher müssen diese in der Parameterliste immer angege-ben werden. Der Funktionsaufruf besteht erwartungsgemäß aus dem Funktions-namen und der Argumentliste:

hello("Oliver")

Wird wie in der hello-Funktion kein Wert zurückgegeben, so hat die Funktionden Ergebnistyp Unit10. Soll der Rückgabetyp in der Funktionsdefinition den-noch explizit angegeben werden, muss vor der öffnenden, geschweiften Klam-mer ein Gleichheitszeichen stehen, d.h. die Funktion hello kann auch definiertwerden durch:

def hello(name: String): Unit = {println("Hello "+name)

}

Eine Funktion mit dem Ergebnistyp Unit wird in Scala Prozedur genannt. Übli-cherweise werden der Typ Unit und das Gleichheitszeichen dann weggelassen.Soll eine Funktion ein Ergebnis berechnen, so muss immer ein Gleichheitszeichenverwendet werden, z.B. bei:

def sum(x: Int, y: Int): Int = {return x+y

}

9 In Scala gibt es die Möglichkeit, mehrere Parameterlisten anzugeben. Was es damit auf sich hat,erfahren Sie in den Abschnitten 4.4 und 5.5.10 Dies entspricht void in Java.

Page 49: Scala : objektfunktionale Programmierung

36 3 Grundlagen

Wird der zuletzt berechnete Ausdruck als Ergebnis zurückgegeben, ist das Schlüs-selwort return optional, d.h. sum kann auch definiert werden durch:

def sum(x: Int, y: Int): Int = {x+y

}

Besteht der Funktionsrumpf aus einem einzigen Ausdruck, so können auch diegeschweiften Klammern weggelassen werden:

def sum(x: Int, y: Int): Int = x+y

Und zu guter Letzt muss der Ergebnistyp einer Funktion in den meisten Fällennicht angegeben werden. Das heißt, die kürzeste Version von sum lautet demnach:

def sum(x: Int, y: Int) = x+y

Der Ergebnistyp kann in den folgenden Fällen nicht automatisch ermittelt wer-den:

1. Die Funktion enthält ein explizites return.

2. Die Funktion ist rekursiv.

3. Die Funktion ist überladen, und eine der Funktionen ruft eine der anderen auf.Dann muss bei der aufrufenden Funktion der Ergebnistyp angegeben werden.

4. Der Typ soll gegenüber dem inferierten explizit eingeschränkt werden.

Ab Version 2.8.0 gibt es named arguments und default arguments. Mit Ersteren ist esmöglich, beim Aufruf einer Funktion die Argumente explizit zu benennen und ineiner anderen Reihenfolge anzugeben. Mit Letzteren können die Argumente, fürdie ein Default-Wert definiert wurde, weggelassen werden. Die folgenden Funk-tionsaufrufe sind alle äquivalent:

sum(1,2)sum(x=1,2)sum(1,y=2)sum(x=1,y=2)sum(y=2,x=1)

Interessant ist insbesondere die letzte Zeile, in der die beiden Argumente ver-tauscht wurden. Wird die Funktion sum mit Default-Argumenten definiert durch:

def sum(x: Int = 1, y: Int = 0) = x+y

kann also auch geschrieben werden:

sum(x=1)sum(y=2)

Der obige Ausdruck sum(x=1,2) ist etwas irreführend, da er nur in dem Spezi-alfall erlaubt ist, in dem trotzdem die Reihenfolge eingehalten wird. Obwohl er

Page 50: Scala : objektfunktionale Programmierung

3.1 Ein kleines bisschen Syntax 37

zum richtigen Ergebnis führt, sollten Sie beim Mischen von benannten Argumen-ten und Positionsargumenten die Regel einhalten, dass alle nicht benannten Argu-mente vor allen benannten Argumenten stehen müssen, denn die Parameterlistewird zuerst der Reihe nach von vorne befüllt, beginnend mit den unbenanntenArgumenten. Die beiden folgenden Ausdrücke sind daher nicht zulässig:

sum(2,x=1) // nicht zulässigsum(y=2,1) // nicht zulässig

Es ist auch möglich, eine variable Anzahl von Parametern zuzulassen. Angezeigtwird dies mit dem * nach Angabe des gemeinsamen Namens für alle Parame-ter und dem Typ eines einzelnen Parameters. Beispielsweise kann die folgendeFunktion mit beliebig vielen Int-Argumenten aufgerufen werden. Jedes Argu-ment wird dann auf einer eigenen Zeile ausgegeben:

def printInts(ints: Int*) {for (int <- ints) println(int)

}

Die hier verwendete for-Schleife wird in Abschnitt 3.2 erklärt. Wie zu sehen ist,verbergen sich hinter dem Namen ints alle Argumente, und zwar in Form einesArrays. Ein Array kann aber nicht direkt übergeben werden, da die Funktiondie Argumente einzeln erwartet. Es kann aber einfach durch die Typangabe _* ineinzelne Werte umgewandelt werden, z.B. funktioniert der folgende Aufruf vonprintInts mit dem Array numbers:

val numbers = Array(1,2,3,4)printInts(numbers: _*)

Dasselbe Verfahren funktioniert auch für andere Collections, beispielsweise:

printInts(List(100,200,300): _*)

Die Funktion kann auch ganz ohne Argumente aufgerufen werden. Fest vorge-gebene Parameter und variable Parameter können auch gemischt werden. Aller-dings müssen die variablen Parameter immer am Ende der Parameterliste stehen,z.B.

def printStringAndInts(string: String, ints: Int*) {// ...

In Scala kann grundsätzlich11 alles ineinander verschachtelt werden, d.h. Funktio-nen können andere Funktionen enthalten. Dadurch ist es beispielsweise möglich,eine Hilfsfunktion, die nur für eine Funktion notwendig ist, direkt in diese zuintegrieren.

11 Nicht ganz alles. Packages dürfen beispielsweise nur Objekte und Klassen enthalten.

Page 51: Scala : objektfunktionale Programmierung

38 3 Grundlagen

Listing 3.3: Eine Funktion, die eine Funktion enthält

def haveSameElements(xs:List[Any],ys:List[Any]) = {def isSubsetOf(xs:List[Any],ys:List[Any]): Boolean = {for (x <- xs)if (!(ys contains x)) return false

true}isSubsetOf(xs,ys) && isSubsetOf(ys,xs)

}

Listing 3.3 zeigt ein Beispiel. Die Funktion haveSameElements überprüft, obzwei Listen dieselben Elemente enthalten, also ob die Repräsentation als Mengegleich ist12. Dazu wird überprüft, ob die eine Liste Teilmenge der anderen ist undumgekehrt. Um redundanten Code für die Teilmengenüberprüfung zu vemeiden,wird eine Funktion isSubsetOf innerhalb von haveSameElements definiert.Diese überprüft mittels einer for-Schleife13 für jedes Element x der ersten Listexs, ob es auch in der zweiten enthalten ist. Sobald ein Element gefunden wurde,das nicht in der zweiten Liste ist, wird false zurückgegeben. Kann die Schlei-fe komplett durchlaufen werden, sind alle Elemente enthalten, und das Ergebnislautet damit true.Nachdem isSubsetOf ein explizites return enthält, muss der ErgebnistypBoolean angegeben werden. Der Typ Any ist Supertyp aller Scala-Typen, d.h.der allgemeinste Typ für jedes Scala-Objekt ist Any. Damit kann die FunktionhaveSameElements auf Listen mit beliebigen Elementen angewendet werden.Wie der Typ mithilfe eines Typparameters eingeschränkt werden kann, erklärenwir in Abschnitt 5.7.Scala hat einen sogenannten Semikolon-Inferenz-Mechanismus, d.h. an den meistenStellen müssen keine Semikolons geschrieben werden. Im Regelfall wird das Zei-lenende als Semikolon interpretiert. Wird ein Ausdruck auf mehrere Zeilen ver-teilt, so muss erkennbar sein, dass der Ausdruck noch nicht abgeschlossen ist.Beispielsweise ist in der ersten Zeile des folgenden Funktionsaufrufs klar, dassder Ausdruck nicht mit dem + enden kann:

hello("Oliver"+" Braun")

Wird die mathematische Funktion f(x, y, z) = x ∗ y + z auf die folgende Weisedefiniert, ist das Ergebnis von f(1,2,3) die Zahl 3 und nicht etwa 5:

def f(x: Int, y: Int, z: Int) = {x * y+ z

}

12 Dies ist ein einfaches Beispiel, um das Konzept der verschachtelten Funktionen zu erklären. Wirkönnen die Funktion auch einfacher definieren durch:def haveSameElements(xs: List[Any], ys: List[Any]) = xs.toSet == ys.toSet13 Die for-Schleife wird auf Seite 41 erklärt.

Page 52: Scala : objektfunktionale Programmierung

3.2 Imperative Programmierung 39

Der Inferenzmechanismus setzt nach dem y ein Semikolon, sodass die Funktion+z als Ergebnis zurückliefert. Richtig gerechnet wird beispielsweise mit der Defi-nition:

def f(x: Int, y: Int, z: Int) = {x * y +

z}

Scala erlaubt die Definition von Typsynonymen mit dem Schlüsselwort type. Bei-spielsweise können Sie nach der Definition:

type IntList = List[Int]

in Ihren Programmen den Typ IntList verwenden. Besonders nützlich sindTypsynonyme, um Programme lesbarer zu machen. Wollen wir beispielsweisein einem Programm eine Preisliste verwalten, die zu jedem Produktnamen denPreis in Cent speichert, können wir dazu zum Beispiel eine Map[String,Int]14

verwenden. Wir können die Map aber auch unter Verwendung folgender Typsyn-onyme nutzen:

type ProductName = Stringtype Cent = Inttype PriceList = Map[ProductName, Cent]

Der vorangegangene Überblick beinhaltete die wesentlichen, aber nicht alleAspekte der Scala-Syntax. Weitere Besonderheiten werden im Zusammenhangmit den jeweiligen Konzepten in den folgenden Kapiteln erläutert. Es folgt nuneine Einführung in die wesentlichen, eingebauten Kontrollstrukturen.

3.2 Imperative Programmierung

Obwohl die Objektorientierung in Scala konsequent umgesetzt wurde, ist es nichtimmer notwendig, Klassen und Objekte zu definieren. In Scala können auch ge-rade für kleine Tasks einfache Scripts erstellt und ausgeführt werden. Anhanddieser Scripts möchten wir Ihnen im Folgenden die imperative Programmierungin Scala vorstellen.Listing 3.4 stellt ein vollständiges Scala-Script dar und kann als Datei mit einembeliebigen Namen, z.B. Hallo.scala, abgespeichert und dann auf der Komman-dozeile mit scala Hallo.scala und einem optionalen Argument ausgeführtwerden.

14 Eine Map ist eine Collection, die Schlüssel-Wert-Paare enthält – im Fall der Preisliste zum SchlüsselProdukt den Wert Preis des Produkts.

Page 53: Scala : objektfunktionale Programmierung

40 3 Grundlagen

Listing 3.4: Hallo.scala

if (args isEmpty)println("Hallo Unbekannte/r")

elseif (args(0) == "Oliver")println("Hi")

elseprintln("Hallo "+args(0))

Kommandozeilenargumente werden in Scala wie in vielen anderen Programmier-sprachen auch über das args-Array übergeben. Wird kein Argument überge-ben, d.h. ist das args-Array leer, so lautet die Ausgabe Hallo Unbekannte/r.Analog zur Infix-Operator-Notation haben wir bei args isEmpty die Postfix-Operator-Schreibweise gewählt. Wir haben sowohl den Punkt als auch die leere Ar-gumentliste weggelassen. Ausführlich könnten wir schreiben args.isEmpty(),wobei es eine Scala-Konvention gibt, die besagt, dass die leeren Klammern weg-gelassen werden sollen, wenn die Methode keine Nebeneffekte hat. Also wäre dienach der Konvention ausführliche Schreibweise args.isEmpty.Werden dem Script Argumente übergeben, so wird das erste Element mit demString Oliver verglichen und anschließend Hi oder Hallo <name> ausge-geben, wobei <name> das erste Kommandozeilenargument ist. In Scala wer-den Objekte mit == verglichen. Wer dennoch lieber equals benutzen möchte,kann dies tun – am besten in der Scala-üblichen Operatornotation name equals"Oliver". Der Vergleichsoperator == ist ein Synonym für equals. Soll die Ob-jektidentität verglichen werden, muss eq benutzt.Die Kontrollstruktur if ist in Scala ein Ausdruck, der den Wert des ausgeführtenZweigs als Ergebnis zurückgibt und damit dem ternären ?:-Operator aus C ent-spricht. Das Hallo-Script kann daher auch wie in Listing 3.5 geschrieben werden.

Listing 3.5: Hallo2.scala

println(if (args isEmpty)"Hallo Unbekannte/r"

elseif (args(0) == "Oliver")"Hi"

else"Hallo "+args(0))

Scala enthält als weitere Kontrollstrukturen auch while- und do-while-Schlei-fen. Listing 3.6 zeigt ein Script, das mithilfe einer while-Schleife die Eingabe desNutzers solange wieder ausgibt, bis quit eingegeben wurde.

Page 54: Scala : objektfunktionale Programmierung

3.2 Imperative Programmierung 41

Listing 3.6: Echo.scala

var input = ""while (input != "quit") {input = readLine()println("Echo: "+input)

}

Sie werden sich sicher langsam wundern, woher die Funktionen println undreadLine kommen. Zwar sehen sie aus wie Builtin-Funktionen, aber es sindganz normale Methoden. Der Trick ist das Predef-Objekt. Dieses Objekt enthältdiese und viele andere nützliche Methoden und wird automatisch in jedes Scala-Programm importiert. Darüber hinaus können die Methoden, wie in den bishe-rigen Listings zu sehen, ohne Angabe des Predef-Objekts geschrieben werden.Das Predef-Objekt wird in Abschnitt 6.1 besprochen.Scala verfügt auch über eine for-Schleife, die aber eher einer foreach-Schleifeentspricht. Listing 3.7 zeigt ein vollständiges Scala-Script, das alle Kommandozei-lenargumente auf einer eigenen Zeile ausgibt.

Listing 3.7: Args.scala

for (arg <- args) println(arg)

Der Ausdruck arg <- args heißt Generator und erzeugt aus args bei jedemSchleifendurchlauf ein neues Element, das der Variable arg zugewiesen wird, bisschließlich das Array vollständig durchlaufen ist. Die eingeführte Schleifenvaria-ble arg verhält sich wie ein val, d.h. innerhalb des Schleifenrumpfs kann argnicht verändert werden.Die klassische for-Schleife mit einer Schleifenvariablen wie z.B. for (int i =0; i<10; i++) gibt es in Scala nicht. Wer dennoch eine Zählvariable benötigt,kann dies beispielsweise wie in Listing 3.8 realisieren.

Listing 3.8: Args2.scala

for (i <- 0 to args.length-1) println(args(i))

In Listing 3.8 entspricht der Ausdruck 0 to args.length-1 dem Methoden-aufruf (0).to(args.length-1), d.h. to ist eine Methode der Klasse Int15

und gibt ein Range-Objekt zurück. Scalas for-Ausdruck ist ein sehr mächtigesWerkzeug, das in Abschnitt 5.6 noch genauer betrachtet werden wird.Geschweifte Klammern definieren in Scala wie auch in vielen anderen Program-miersprachen einen sogenannten scope, einen Gültigkeitsbereich. Wird innerhalbeines solchen Scopes etwas definiert, z.B. eine Variable oder eine Funktion, so istes nur darin gültig. Außerhalb des Scopes gibt es den Bezeichner einfach nicht

15 Genauer gesagt ist to eine Methode der Klasse RichInt, und ein Int wird implizit zu einemRichInt gemacht, wenn notwendig. Die Erklärung dazu finden Sie in Kapitel 4.

Page 55: Scala : objektfunktionale Programmierung

42 3 Grundlagen

mehr. Da geschweifte Klammern ineinander geschachtelt werden können, gibt esScopes, die Scopes enthalten. Wird in einem inneren Scope ein Bezeichner verwen-det, der bereits in dem äußeren Scope genutzt wurde, so überschattet die innereDefinition die äußere.

Listing 3.9: Eine Funktion mit einer verschatteten Variablen

def fun(x: Int) = {if (x == 3) {val x = 7println(x)

}println(x)

}

Betrachten wir als Beispiel die Funktion in Listing 3.9. Obwohl in der Funktionnur println(x) vorkommt, wird beim Aufruf von fun(3) zuerst eine 7, danneine 3 ausgegeben. In der gesamten Funktion ist der Bezeichner x gültig, der beimAufruf fun(3) den Wert 3 hat. Im ersten Zweig der if-Anweisung wird mit dengeschweiften Klammern ein neuer Scope erzeugt. In diesem wird eine neues xdefiniert. Dieses überschattet das äußere x mit dem Wert 3. Daher wird dort eine7 ausgegeben. Nach der schließenden geschweiften Klammer verliert das innerex seine Gültigkeit. Daher wird bei dem zweiten println(x) eine 3 ausgegeben.

3.3 Ein ausführbares Programm

Bisher haben wir Scala-Scripts geschrieben, die sich mit scala <filename>ausführen lassen. Mit den Scala-Compilern scalac und fsc können aber auchausführbare Programme erzeugt werden. Je nachdem, wie kompiliert wurde, istfür die Ausführung eine Java Virtual Machine oder eine .NET-Umgebung not-wendig.Ein Scala-Script kann nicht direkt in ein ausführbares Programm übersetzt wer-den. Ausführbar ist in Scala ein Objekt, das die Methode main implementiert.Objekte und Methoden werden in Kapitel 4 eingeführt. An dieser Stelle wollenwir nur kurz und knapp die notwendige Syntax vorstellen.Die einfachste Möglichkeit, ein Script übersetzbar zu machen, ist das sogenannteErweitern des Application-Trait16. Listing 3.10 zeigt die Implementierung desEcho-Scripts aus Listing 3.6 auf Seite 41 als Application.

Listing 3.10: Ein ausführbares Echo-Programm

object Echo extends Application {var input = ""

16 Auch hier müssen wir Sie mit den Details leider auf Kapitel 4 vertrösten.

Page 56: Scala : objektfunktionale Programmierung

3.3 Ein ausführbares Programm 43

while (input != "quit") {input = readLine()println("Echo: "+input)

}}

Im Gegensatz zum Echo-Script sind zwei Zeilen hinzugekommen: die erste unddie letzte. Damit wird ein Objekt mit dem Namen Echo definiert (object Echo),das den Application-Trait erweitert (extends Application). Alles, waszum Objekt gehört, muss dann innerhalb eines Paars geschweifter Klammern ge-schrieben werden.Übersetzt wird die Quellcode-Datei dann mit

$ scalac Echo.scala

bzw.

$ fsc Echo.scala

Zum Ausführen wird der Objektname dem Kommando scala als Argumentübergeben:

$ scala EchoHallo WeltEcho: Hallo WeltHallo LeserEcho: Hallo LeserquitEcho: quit

Die vorgestellte Erweiterung des Application-Traits hat allerdings einige Ein-schränkungen. Wesentlich an dieser Stelle ist, dass das Array args mit den Kom-mandozeilenargumenten so nicht verfügbar ist. Das heißt, der Versuch, aus Lis-ting 3.7 auf Seite 41 durch Erweiterung des Application-Traits ein ausführbaresProgramm zu machen, schlägt mit folgender Fehlermeldung beim Übersetzenfehl:

$ fsc Args.scalaArgs.scala:2: error: not found: value argsfor (arg <- args)

^one error found

Programme, die aus mehreren Threads bestehen, können den Application-Traitnicht verwenden, da es damit zu sogenannten Deadlocks17 kommt. Und zu gu-ter Letzt sind einige JVM-Implementierungen nicht in der Lage, den Code zuoptimieren, der vom Application-Trait ausgeführt wird. Daher wird die Ver-wendung des Application-Traits nur für ganz einfache, aus einem einzelnenThread bestehende Programme empfohlen.

17 siehe z.B. [Tan09]

Page 57: Scala : objektfunktionale Programmierung

44 3 Grundlagen

Ohne Application-Trait muss eine main-Methode in dem Objekt, das ausge-führt werden soll, implementiert werden. Der Rumpf dieser Methode wird dannbeim Starten ausgeführt. Die main-Methode hat als einzigen Parameter ein Arrayaus Strings, über das die Kommandozeilenargumente übergeben werden können,und kein Ergebnis, also den Ergebnistyp Unit. Listing 3.11 zeigt die Implemen-tierung des Code aus Listing 3.7 als ausführbares Programm.

Listing 3.11: Ein ausführbares Programm mit einer expliziten main-Methode

object Args {def main(args: Array[String]) {for (arg <- args)println(arg)

}}

Das übersetzte Objekt kann dann mit dem Kommando scala ausgeführt werden:

$ scala Args "Hallo Welt" und "Hallo Leser"Hallo WeltundHallo Leser

3.4 Annotations

Scala unterstützt annotations, um Metadaten zu einer Deklaration hinzuzufügen.Annotations werden sowohl in Java als auch in .NET genutzt, und insbesondereim Bereich von Enterpriseanwendungen sind sie kaum mehr wegzudenken. Java-und .NET-Annotations können direkt im Scala-Code verwendet werden.Annotations beginnen mit einem @-Zeichen, gefolgt vom Namen der Annotation.Dem Namen kann eine Argumentliste in runden Klammern folgen. Mit Scala 2.8können Annotations auch ineinander verschachtelt werden. Typische Annotati-ons in Scala sind

@serializable class C { ... }@transient @volatile var m: Int@deprecated("Use g instead") def f = ...(e: @unchecked) match { ... }

Annotations können also vor Klassen oder Variablen- bzw. Funktionsdefinitio-nen stehen oder hinter einem Ausdruck und einem Doppelpunkt. Darüber hinausgibt es noch die Möglichkeit, eine Annotation hinter einen Typ zu schreiben, z.B.String @local. Einige Annotations, z.B. @throws, @unchecked, @tailrecund @specialized, werden wir Ihnen noch im Verlauf des Buches vorstellen.

Page 58: Scala : objektfunktionale Programmierung

3.4 Annotations 45

Im Gegensatz zu Java fällt aber schon an den obigen Beispielen auf, dass Scala anmanchen Stellen Annotations verwendet, wofür es in Java ein Schlüsselwort gibt:transient oder volatile.18

Annotations können auch ganz einfach ohne spezielle Syntax selbst definiert wer-den. Sie müssen einfach nur eine Klasse schreiben, die von Annotation abge-leitet ist19. Die Nutzung der Annotation mit @<annotationname> entsprichtdann dem Konstruktoraufruf dieser Klasse. Es gibt bereits zwei spezialisierteVersionen, von denen Sie ableiten können: scala.ClassfileAnnotation undscala.StaticAnnotation. Eine ClassfileAnnotation wird nur in der er-zeugten Class-Datei abgelegt. Eine StaticAnnotation überall dort, wo ein an-notierter Bezeichner genutzt wird.

18 Es gibt auch den umgekehrten Fall, wie wir später noch sehen werden: Java verwendet die Annotati-on @Override für die Redefinition von Members, in Scala gibt es dafür das Schlüsselwort override.19 Wie Sie Klassen schreiben und von etwas ableiten, erklären wir in Kapitel 4.

Page 59: Scala : objektfunktionale Programmierung
Page 60: Scala : objektfunktionale Programmierung

Kapitel 4

Reine Objektorientierung

Scala ist eine rein objektorientierte Programmiersprache1 mit einem einheitlichenObjekt-Modell. Das heißt, jeder Wert ist ein Objekt, und jede Operation ist eineNachricht an ein Objekt. In diesem Kapitel werden zunächst in Abschnitt 4.1 dieGrundbausteine, Klassen und Objekte vorgestellt. Anschließend gehen wir in Ab-schnitt 4.2 kurz auf die Organisation des Codes ein. Scala hat keine Interfaces,sondern sogenannte Traits (siehe Abschnitt 4.3). Traits können neben Methoden-signaturen bereits Implementierungen und auch Felder enthalten. Den Abschlussdes Kapitels bildet der Abschnitt 4.4 über die impliziten Umwandlungen und Pa-rameter und die sogenannten Rich-Wrapper, mithilfe derer existierende Klassenerweitert werden können.

4.1 Klassen und Objekte

Der Kern der Objektorientierung sind Objekte und Klassen als Baupläne für Ob-jekte. Im Gegensatz zu vielen anderen objektorientierten Programmiersprachenkönnen in Scala nicht nur Klassen mit dem Schlüsselwort class, sondern auchsogenannte Singleton-Objekte, also eigenständige Objekte ohne zugehörige Klassemit dem Schlüsselwort object programmiert werden.

4.1.1 Felder und Methoden

Objekte und Klassen können Felder und Methoden, zusammengefasst unter demNamen Member, enthalten. Felder werden wie Variablen mit var oder val defi-niert, je nachdem ob sie veränderbar sein sollen. Methoden sind zum Objekt bzw.zur Klasse gehörende Funktionen und werden daher mit def eingeleitet. AlleMember einer Klasse oder eines Objektes stehen auf oberster Ebene. Das heißt,

1 Als eine der besten Einführungen in die objektorientierte Programmierung (OOP) gilt [Mey00].

Page 61: Scala : objektfunktionale Programmierung

48 4 Reine Objektorientierung

Member können keine anderen Member, selbstverständlich aber Funktionen undVariablen enthalten.

Listing 4.1: Das Objekt Hello

object Hello {var greeting = "Hello"def hello() {println(greeting)

}}

Listing 4.1 definiert ein Objekt (Schlüsselwort object) mit dem Namen Hellodas ein Feld2 und eine Methode besitzt. Dem Feld greeting wird der StringHello zugewiesen. Aufgrund der Typinferenz muss der Typ String aber nichtexplizit angegeben werden. Die Methode hello hat keine Argumente und gibtden Wert des Feldes greeting mit der Predef-Methode println auf der Kon-sole aus. Wird das Objekt beispielsweise in der interaktiven Scala-Shell definiert3,so kann anschließend damit gearbeitet werden, wie die folgende Sitzung zeigt:

scala> Hello.hello()Hello

scala> Hello.greeting="Hi"

scala> Hello.hello()Hi

In obiger Sitzung wurde zunächst die Nachricht hello an das Objekt Hello ge-sendet, welches dann mit der Ausführung seiner Methode hello reagiert. An-schließend wurde dem Feld greeting der String Hi zugewiesen. Die erneuteAusführung von hello schreibt dann wie erwartet Hi auf die Konsole.Die Definition einer Klasse unterscheidet sich von der eines Objektes zunächsteinmal nur in der Nutzung des Schlüsselwortes class statt object. In Listing4.2 ist die Klasse Hello definiert, die bis auf das Schlüsselwort class identischzum Objekt Hello aus Listing 4.1 ist.

Listing 4.2: Die Klasse Hello

class Hello {var greeting = "Hello"def hello() {println(greeting)

2 Das veränderbare Feld wird an dieser Stelle nur genutzt, um den Zugriff in der Scala-Shell zu de-monstrieren.3 Das heißt, entweder wird der Code aus Listing 4.1 direkt eingegeben oder in eine Datei geschrieben,z.B. mit dem Namen Hello.scala, und diese mit dem Kommandozeilenschalter -i Hello.scalabeim Starten der Shell oder mit :load Hello.scala in den laufenden Interpreter geladen.

Page 62: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 49

}}

Mit der Implementierung der Klasse passieren zwei Dinge. Zum einen können mitdem Schlüsselwort new nun beliebig viele Objekte mit den in der Klasse definier-ten Feldern und Methoden erzeugt werden. Zum anderen wird durch die Klasseein Typ definiert. Variablen können also nun z.B. vom Typ Hello sein. Funktio-nen können Werte vom Typ Hello als Argument akzeptieren oder als Ergebniszurückliefern.Um die Klasse Hello zu nutzen, müssen also zuerst Objekte mit dem Schlüs-selwort new erzeugt werden. Jedes dieser Objekte hat dann ein eigenes Feldgreeting und reagiert auf die Nachricht hello mit der Ausführung der Me-thode hello. Variablen, denen die Objekte zugewiesen werden, haben den TypHello. Die folgende Sitzung zeigt die Erzeugung und Verwendung von zweiHello-Objekten:

scala> val say = new Hellosay: Hello = Hello@473eae6e

scala> say.hello()Hello

scala> val sayAgain = new HellosayAgain: Hello = Hello@569764bd

scala> say.greeting="Hi"

scala> say.hello()Hi

scala> sayAgain.hello()Hello

Zunächst wird eine unveränderliche Variable say definiert, ein neues Hello-Objekt erzeugt und dieses der Variable say zugewiesen. Damit wird für die Va-riable say der Typ Hello inferiert. Anschließend wird die Nachricht hello andas Objekt mit dem Namen say geschickt, das mit der Ausgabe der Zeichen-kette Hello reagiert. Mit der dritten Eingabe werden ein zweites Objekt und ei-ne zweite Variable erzeugt. Wie zu erwarten ist, bleibt der Begrüssungstext dessayAgain-Objekts Hello, obwohl der des say-Objektes mit der darauf folgen-den Eingabe auf Hi geändert wurde, da jedes Objekt eigene Felder, sogenannteInstanzvariablen, hat.An dieser Stelle wird auch noch einmal deutlich, dass eine unveränderliche Varia-ble durchaus ein veränderliches Objekt referenzieren kann. Das Feld greetingvon say kann verändert werden. say kann aber kein anderes Objekt referenzie-ren. Der Versuch say = sayAgain wird mit error: reassignment to valquittiert.

Page 63: Scala : objektfunktionale Programmierung

50 4 Reine Objektorientierung

Wird in der Scala-Shell ein Objekt mit new erzeugt, wird es mithilfe der to-String-Methode ausgegeben. Nachdem die Klasse Hello keine eigene to-String-Methode besitzt, wird die Standardimplementierung genutzt. Da wir unsauf der Java-Plattform befinden, handelt es sich um die Implementierung ausjava.lang.Object, die den Klassennamen, ein @-Zeichen und die hexadezi-male Repräsentation des Hash-Codes des Objekts ausgibt.Um eine eigene toString-Methode zu implementieren, muss sie redefiniert wer-den. Die Klasse Hello kann beispielsweise um die folgende Methodendefinitionergänzt werden:

override def toString = "Ready to say "+greeting

Beim Redefinieren von Methoden ist der Modifier override4 zwingend vor-geschrieben. Wird er weggelassen, wird dies vom Scala-Compiler mit folgenderMeldung quittiert:

<console>:13: error: overriding method toString in classObject of type ()java.lang.String;

method toString needs ‘override’ modifierdef toString = "Ready to say "+greeting

Die Methode toString kann nun explizit aufgerufen werden bzw. sie wird beimAusgeben des Objektes mit println automatisch ausgeführt. Nur in der Scala-Shell wird sie beim Aufruf des Konstruktors automatisch genutzt. Die folgendeSitzung stellt dies beispielhaft dar:

scala> val say = new Hellosay: Hello = Ready to say Hello

scala> println(say)Ready to say Hello

scala> say.greeting="Hi"

scala> println(say)Ready to say Hi

Sehen wir uns nun noch einmal das Feld greeting an. Derzeit kann es durcheinfache Zuweisung beliebig verändert werden. Um keine Änderung zuzulassen,ersetzen wir in der Definition einfach nur das var durch ein val. In vielen Fällenist aber ein Mittelweg zwischen keiner und beliebiger Änderung das Gewünschte.In vielen objektorientierten Programmiersprachen wird dazu das Feld verstecktund Zugriff darauf nur über sogenannte Getter- und Settermethoden zugelassen.Scala geht einen für den Programmierer einfacheren Weg: Jedes Feld, das mit demSchlüsselwort var definiert wurde, wird direkt als Paar von Getter- und Setter-methode interpretiert. Das heißt, das Feld wird automatisch versteckt und eineGettermethode mit demselben Namen sowie eine Settermethode mit dem Namen

4 Java hat eine @Override-Annotation, die nicht zwingend vorgeschrieben ist.

Page 64: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 51

ergänzt um die Zeichen _=. Lesender Zugriff auf das Feld führt dann die Getter-methode aus, schreibender Zugriff die Settermethode. Um dennoch die üblicheSyntax <varname>.<fieldname> = <value> beizubehalten, wird darausautomatisch <varname>.<fieldname>_=(<value>) gemacht.Wird beispielsweise auf das Feld greeting lesend zugegriffen, so wird die Get-termethode greeting ausgeführt. Wird dem Feld greeting z.B. mit

say.greeting="Hi"

ein Wert zugewiesen, so wird die Settermethode greeting_= mit der rechtenSeite des Gleichheitszeichens als Argument, d.h. also

say.greeting_=("Hi")

ausgeführt.Getter- und Settermethoden können natürlich auch selbst implementiert werden.Listing 4.3 zeigt ein Beispiel. Das eigentliche Feld heißt in dem Fall greet undwird mit dem Modifier private[this]5 versteckt. Zugriff auf das Feld erfolgtdurch die Methoden greeting und greeting_=. Die bisher gezeigten Beispielein der Scala-Shell laufen unverändert weiter.

Listing 4.3: Die Klasse Hello

class Hello {private[this] var greet = "Hello"def greeting = greetdef greeting_=(greeting: String) {if (greet == greeting)println("greeting is already set to \""+greet+"\"")

else {greet=greetingprintln(this)

}}// ... the rest goes here

}

Wird nun auf greeting lesend zugegriffen, wird der aktuelle Wert des Feldesgreet zurückgegeben. Beim schreibenden Zugriff wird überprüft, ob der aktu-elle Wert von greet bereits mit dem Parameter greeting übereinstimmt. Istdies der Fall, wird dies ausgegeben. Andernfalls wird der übergebene Wert demFeld greet zugewiesen und das Objekt mit println ausgegeben. Um dies zuverdeutlichen, ist im Folgenden wieder eine Beispielsitzung mit der Scala-Shellangegeben:

scala> val say = new Hellosay: Hello = Ready to say Hello

5 Die sogenannten Sichtbarkeitsmodifier werden auf Seite 56 erklärt.

Page 65: Scala : objektfunktionale Programmierung

52 4 Reine Objektorientierung

scala> println(say.greeting)Hello

scala> say.greeting = "Hi"Ready to say Hi

scala> say.greeting = "Hi"greeting is already set to "Hi"

Nachdem sich in Scala Felder und Methoden einen Namensraum teilen, kann dasversteckte Feld nicht auch greeting heißen. Lokale Variablen und Parameterkönnen aber, wie in Listing 4.3 zu sehen, denselben Namen wie Member benut-zen. Um in solch einem Fall dennoch auf das verschattete Member zugreifen zukönnen, muss das Objekt explizit mit this referenziert werden. Beispielsweisekönnte statt der Zeile

if (greet == greeting)

in Listing 4.3 die Zeile

if (this.greeting == greeting)

verwendet werden und damit innerhalb der Settermethode die Gettermethodezum Zugriff auf das Feld greet genutzt werden.Ein Paar von Getter- und Settermethode wird auch als Property bezeichnet – in An-lehnung an die Properties von C#, die jedoch eine spezielle Syntax haben. Getter-und Settermethoden können auch ohne assoziierte Variable genutzt werden. Lis-ting 4.4 zeigt ein Beispiel6. Definiert werden ein Feld euro und eine Propertydollar.

Listing 4.4: Die Klasse Currency

class Currency {var exchangeRate = 2.0fvar euro = 0.0fdef dollar = euro * exchangeRatedef dollar_= (m: Float) {euro = m / exchangeRate

}override def toString = euro+" EUR / "+dollar+" USD"

}

Statt der Variablen euro den Wert 0.0f explizit zuzuweisen, können wir dies,weil es dem Standardwert für den Datentyp Float entspricht, auch automatisch

6 Die Klasse Currency dient als einfaches Beispiel, um einige Konzepte von Scala zu verdeutlichen,und ist bewusst einfach gehalten. Sollten Sie eine Repräsentation von Geld für eine Anwendung benö-tigen, die tatsächlich zum Einsatz kommt, verwenden Sie nie eine Gleitpunktzahl, da dort Rundungs-fehler auftreten können. Der angegebene Kurs (exchangeRate) entspricht zugegebenermaßen auchnicht dem aktuellen Stand, macht die folgenden Beispiele aber übersichtlicher.

Page 66: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 53

setzen lassen. Dazu ist es in Scala allerdings notwendig, einen Unterstrich zuzu-weisen. Nachdem dann aber kein Typ mehr lokal inferiert werden kann, mussdieser angegeben werden. Das heißt, wir könnten die zweite Zeile in der KlasseCurrency durch folgende Zeile ersetzen:

var euro: Float = _

Ohne die Zuweisung des Unterstrichs entspräche euro einer abstrakten Varia-blen und Currency dadurch einer abstrakten Klasse (siehe Abschnitt 4.1.6). DieStandardwerte für die verschiedenen Datentypen sind 0 für die numerischen Da-tentypen, false für den Datentyp Boolean, () für den Typ Unit und null füralle Referenztypen.Für den Nutzer der Klasse Currency sieht es nun so aus, als hätte die Klassezwei Felder: euro und dollar. In Wirklichkeit wird nur ein Wert im Feld eurogespeichert und beim Zugriff über dollar jedes Mal umgerechnet. Eine Beispiel-sitzung sieht folgendermaßen aus:

scala> val m1, m2 = new Currencym1: Currency = 0.0 EUR / 0.0 USDm2: Currency = 0.0 EUR / 0.0 USD

scala> m1.euro = 20

scala> m1res0: Currency = 20.0 EUR / 40.0 USD

scala> m2.dollar = 20

scala> m2res1: Currency = 10.0 EUR / 20.0 USD

Als Erstes werden zwei Values m1 und m2 definiert und jedem der beiden Werteein eigenes Currency-Objekt zugewiesen. Scala lässt auch an dieser Stelle eineabkürzende Schreibweise zu. Anschließend wird dem Objekt m1 der Eurowert 20zugewiesen, und beide Werte werden ausgegeben. Umgekehrt wird bei m2 derDollarwert gesetzt.Die Klasse Currency enthält ein weiteres Feld, in dem der aktuelle Kurs gehaltenwird. In Listing 4.4 haben wir das Feld als var definiert, also kann es von außenbeliebig geändert werden. Eine Änderung des Kurses bedeutet in diesem Fall eineÄnderung des Dollar-, aber keine Änderung des Eurowertes. Hätten wir umge-kehrt ein Feld dollar und eine Property euro, so bliebe der Dollarwert stabil.Wenn wir weiter überlegen, muss der Kurs natürlich für alle Currency-Objektedenselben Wert haben. Wenn er sich ändert, muss er in allen existierenden Objek-ten geändert werden, und neue Objekte müssen direkt mit dem neuen Kurswerterzeugt werden.Die Lösung wäre, den Kurs nicht in jedem Objekt zu speichern, sondern einmalan zentraler Stelle zu halten. Eine solche zentrale Stelle für Objekte einer Klas-

Page 67: Scala : objektfunktionale Programmierung

54 4 Reine Objektorientierung

se ist das sogenannte Companion-Objekt. Das Companion-Objekt hat den gleichenNamen wie die Klasse und muss zusammen mit der Klasse in einer Quellcode-Datei stehen. Dieses Singleton-Objekt ersetzt die statischen Felder und Methodenvon Java und anderen objektorientierten Programmiersprachen zugunsten eineseinheitlichen Objektmodells.Eine überarbeitete Version der Currency-Klasse mit einem Companion-Objekt istin Listing 4.5 dargestellt.

Listing 4.5: Die Klasse Currency und das Objekt Currency

object Currency {var exchangeRate = 2.0f

}class Currency {var euro = 0.0fdef dollar = euro * Currency.exchangeRatedef dollar_= (m: Float) {euro = m / Currency.exchangeRate

}override def toString = euro+" EUR / "+dollar+" USD"

}

Auf den Kurs im Companion-Objekt kann nun mit Currency.exchangeRatezugegriffen werden. Wird der Kurs im Companion-Objekt geändert, hat dies wiegewünscht Auswirkungen auf alle existierenden und alle neuen Objekte, wie diefolgende Sitzung zeigt:

scala> val m1 = new Currencym1: Currency = 0.0 EUR / 0.0 USD

scala> m1.euro = 20

scala> m1res0: Currency = 20.0 EUR / 40.0 USD

scala> Currency.exchangeRate = 4

scala> m1res1: Currency = 20.0 EUR / 80.0 USD

scala> val m2 = new Currencym2: Currency = 0.0 EUR / 0.0 USD

scala> m2.dollar = 20

scala> m2res2: Currency = 5.0 EUR / 20.0 USD

Page 68: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 55

Nachdem der Kurs auf 4.0 geändert wurde, ist der Dollarwert von m1 von 40 auf80 Dollar gestiegen. Für das nach der Kursänderung erzeugte Objekt m2 gilt sofortder neue Kurs.Wir kehren nun noch einmal zurück zu den Methoden und betrachten erneut dieKlasse Hello. Der Übersichtlichkeit halber setzen wir noch einmal mit der erstenVersion der Klasse aus Listing 4.2 auf Seite 48, mit einem Feld greeting undeiner Methode hello, die den Begrüßungstext mithilfe von println ausgibt,an.Wir wollen die Klasse nun so erweitern, dass wir mit Namen begrüßt werdenkönnen. Ein erster Ansatz wäre, die Klasse durch eine Methode helloName zuerweitern, die wie folgt aussehen würde:

def helloName(name: String) {println(greeting+" "+name)

}

Damit könnten wir nun z.B. Hello und Hello Oliver sagen, wie die folgendeSitzung zeigt:

scala> val say = new Hellosay: Hello = Hello@54373e38

scala> say.hello()Hello

scala> say.helloName("Oliver")Hello Oliver

Schöner wäre es natürlich, wenn wir nicht einmal hello und das andere MalhelloName nutzen müssten, sondern in beiden Fällen einfach denselben Namenhello verwenden könnten. Das ist auch möglich und nennt sich Überladen vonMethoden. Beim Überladen von Methoden ist darauf zu achten, dass sich die Pa-rameterlisten aller Methoden mit demselben Namen bezüglich Anzahl oder Typunterscheiden. Die Methoden können auch verschiedene Ergebnistypen haben.Dies alleine reicht aber nicht zur Unterscheidung aus und wird vom Compilernicht akzeptiert.

Listing 4.6: Die Klasse Hello mit zwei Methoden hello

class Hello {var greeting = "Hello"def hello() {println(greeting)

}def hello(name: String) {println(greeting+" "+name)

}}

Page 69: Scala : objektfunktionale Programmierung

56 4 Reine Objektorientierung

Listing 4.6 zeigt die Klasse Hello, die zwei Methoden mit dem Namen hello be-sitzt. Die erste Methode hat keinen, die zweite Methode einen Parameter vom TypString. Damit kann beim Aufruf der Methode über die Argumentliste entschie-den werden, welche der beiden ausgeführt wird. Ist die Argumentliste leer, wirddie erste Methode ausgeführt. Enthält die Argumentliste genau einen String bzw.einen Ausdruck, der zu einem String ausgewertet werden kann, wird die zwei-te Methode ausgeführt. In allen anderen Fällen findet der Compiler einen Fehler,und das Programm wird nicht übersetzt.Bisher haben wir alle Member einer Klasse ohne einen Sichtbarkeitsmodifier ange-geben. Dadurch sind die Member alle public, das heißt aus jeder anderen Klassesichtbar und nutzbar.Ein wesentliches Merkmal der objektorientierten Programmierung ist das informa-tion hiding, also das Verstecken der Implementierung einer Klasse. Natürlich nicht,weil es ein Geheimnis ist, sondern um bei einer neuen Version der Klasse die Im-plementierung ändern zu können, ohne Gefahr zu laufen, dass Applikationen, diedie Klasse nutzen, nicht mehr kompilierbar sind. Ziel ist es, das Application Pro-gramming Interface (API) einer Klasse stabil zu halten, wobei dazu automatisch alleöffentlichen Member gehören.Scala bietet zum Einschränken der Sichtbarkeit von Membern die Modifierprivate und protected. Diese werden vor den entsprechenden Schlüsselwör-tern angegeben, z.B.

private var x = ...protected class Helper { ... }

Ist ein Member einer Klasse oder eines Objektes mit private gekennzeichnet,so kann nur innerhalb der Klasse bzw. des Objektes darauf zugegriffen werden.Nachdem Scala auch das Verschachteln von Klassen zulässt, gilt der Zugriffs-schutz auch für innere Klassen. Dabei gilt, dass aus einer weiter innen liegendenKlasse auf die mit private geschützten weiter außen liegenden Member zuge-griffen werden kann. Umgekehrt gilt das allerdings nicht und weicht damit zumBeispiel von Java ab, wo in einer Klasse grundsätzlich alle Member einer innerenKlasse sichtbar sind.Für mit protected geschützte Member gilt, dass sie über eine Klasse hinaus nurin abgeleiteten Klassen sichtbar sind und nicht in allen Klassen des Packages wiein Java. Abgeleitete Klassen werden in Abschnitt 4.1.5 besprochen.Es ist darüber hinaus möglich, die Sichtbarkeit auf eine noch feingranularere Wei-se zu definieren. Dazu gibt es sogenannte Qualifier, die in eckigen Klammern nachprivate oder protected stehen, z.B. private[X] oder protected[Y]. Xbzw. Y stehen dabei für Packages, Klassen oder Singleton-Objekte. Mit dem Modi-fier private[this] ist es möglich, den Zugriff auf ein Member so einzuschrän-ken, dass nur innerhalb des Objekts selbst und nicht aus anderen Objekten derselben Klasse darauf zugegriffen werden kann, wie bei private.

Page 70: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 57

Das in Listing 4.5 auf Seite 54 gezeigte Companion-Objekt hätte in dieser Kon-stellation auch ein beliebiges anderes Singleton-Objekt sein können. Dann müssteaus der Currency-Klasse auf den Wechselkurs eben nicht mit dem AusdruckCurrency.exchangeRate, sondern über den Namen des anderen Objektes zu-gegriffen werden.Interessant werden Companion-Objekte, wenn es um Zugriffsschutz geht. Es giltnämlich für eine Klasse und ihr Companion-Objekt Folgendes: Aus der Klassekann auf alle mit dem private-Modifier versehenen Member des Companion-Objektes zugegriffen werden und umgekehrt.

Listing 4.7: SharedCounter.scala

class SharedCounter {private[this] var counted = 0def count() {SharedCounter.increment()counted += 1

}override def toString ="Current shared value is "+SharedCounter.value+". Incremented shared counter "+counted+" times."

}object SharedCounter {private[this] var count = 0private def value = countprivate def increment() {count += 1

}}

Listing 4.7 zeigt zur Veranschaulichung eine Klasse SharedCounter mit demdazugehörigen Companion-Objekt7. Die Zählvariable count befindet sich imCompanion-Objekt und ist aufgrund von private[this] auch nur dort sicht-bar. Die beiden Methoden value und increment sind außerdem in der Klas-se SharedCounter sichtbar. Eine SharedCounter-Instanz kann den gemeinsa-men Zähler mit SharedCounter.increment() hochzählen und auf den aktu-ellen Stand mittels SharedCounter.value zugreifen. Darüber hinaus hat jedeSharedCounter-Instanz ein objekt-privates Feld counted, in dem festgehaltenwird, wie oft diese Instanz den gemeinsamen Zähler inkrementiert hat. Auf diesesFeld kann nur innerhalb der entsprechenden Instanz zugegriffen werden.Aufgrund von Limitierungen der Scala-Shell lässt sich der Code aus Listing 4.7weder in die Shell eingeben noch als Datei laden. Es muss kompiliert und kannbeispielweise mit einer einfachen, wie in Listing 4.8 dargestellten, Applikationgetestet werden.

7 Zur Erinnerung: Die Klasse SharedCounter und das Objekt SharedCounter müssen sich in ei-ner Quellcode-Datei befinden. Die Verwendung des gleichen Namens alleine reicht nicht aus, um einCompanion-Objekt zu definieren.

Page 71: Scala : objektfunktionale Programmierung

58 4 Reine Objektorientierung

Listing 4.8: Die Applikation CounterTest zum Testen des SharedCounters

object CounterTest extends Application {val sc1 = new SharedCounterval sc2 = new SharedCounterfor (i <- 1 to 10) {sc1.countprintln(sc1)sc2.countprintln(sc2)

}}

Eine weitere Besonderheit von Scala wird in Listing 4.7 beim Blick auf die beidenMethoden value und increment sichtbar. Beide Methoden haben keine Para-meter, aber nur hinter increment steht ein leeres Klammerpaar. Die Methodevalue ist eine parameterless method und wird ohne leeres Klammerpaar geschrie-ben. Die Methode increment dagegen ist eine empty-paren method.Bedeutung bekommt der Unterschied durch eine Konvention: Wenn eine Metho-de keine Parameter hat und ausschließlich lesend auf veränderbare Daten zu-greift, so sollte sie als parameterlose Methode definiert werden. Hat eine Metho-de Nebeneffekte wie beispielsweise Input/Output oder Veränderung einer Varia-blen, so soll die Methode mit leerem Klammerpaar geschrieben werden.Mit der Möglichkeit das leere Klammerpaar weg zu lassen, wird das uniform accessprinciple8 unterstützt, nach dem es für die Verwendung einer Klasse transparentsein muss, ob ein Attribut als Feld oder Methode implementiert wird.Nachdem bei Java-Methoden immer Klammern geschrieben werden müssen undScala sehr gut mit Java interoperiert, können in Scala leere Klammern sehr liberalverwendet bzw. weggelassen werden. Es ist also grundsätzlich auch möglich, stattprintln() zum Ausgeben eines Zeilenumbruchs println ohne Klammern zuschreiben. Dies sollte aber aufgrund des Nebeneffekts der Methode nicht gemachtwerden.Wird umgekehrt eine parameterlose Methode definiert, stellt der Compiler sicher,dass sie immer ohne leeres Klammerpaar genutzt wird. Ansonsten würde einespätere Änderung der Methode in ein Feld einen Fehler verursachen, da auf einFeld natürlich nie mit einem leeren Klammerpaar zugegriffen werden darf.Scala unterscheidet nicht wie viele andere Sprachen zwischen Methoden undOperatoren. Das, was wie ein Operator aussieht, ist in Wirklichkeit auch einfachnur eine Methode. Werden beispielsweise zwei Zahlen addiert, so schreiben wirin fast allen Sprachen 1 + 2. In Scala entspricht dies einem Methodenaufruf derMethode + des Objektes 1, also eigentlich (1).+(2)9. In Kapitel 3 auf Seite 31

8 siehe [Mey00]9 Die Klammern um die Zahl 1 werden für den Compiler benötigt, der sonst denkt, es handelt sich umdie Gleitpunktzahl 1.0, die abgekürzt als 1. geschrieben werden darf.

Page 72: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 59

sind die zulässigen Bezeichner aufgezählt, die auch für Methodennamen genutztwerden können.Nachdem Operatoren nichts Besonderes sind, ist die sogenannte Infix-Operator-Notation, also die Schreibweise <object> <operator> <argument>, auchnicht auf Operatoren eingeschränkt. Eine beliebige Methode mit einem Argumentkann in Infix-Operator-Notation geschrieben werden und wird damit zu einemOperator, z.B. "Hello World" substring 6. Hat die Methode mehr als einArgument, müssen die Klammern um die Argumentliste erhalten bleiben, z.B.bei "Hello World" substring (6,7). Methoden ohne Argument könnenin Postfix-Operator-Notation geschrieben werden, z.B. "Hallo" toUpperCase.Hat die Methode Nebeneffekte und wird deshalb per Konvention die Klammergeschrieben, sollte die Postfix-Operator-Notation natürlich nicht genutzt werden.Für die Präfix-Operator-Notation, also das Angeben der Methode vor dem Ob-jekt, gibt es allerdings Einschränkungen. In Präfixschreibweise dürfen ausschließ-lich die Operatoren +, -, ! und ˜ geschrieben werden. Diese Operatoren sindsogenannte unäre Operatoren, haben also nur einen Operand. Der Name der zumPräfix-Operator <op> gehörenden Methode lautet unary_<op>. Das heißt also,der Ausdruck -3 entspricht dem Ausdruck (3).unary_-. Hätte die Methodeden Namen - ohne das unary_-Präfix, dürfte sie in Postfix-Operator-Notationgenutzt werden.In Scala erfahren Arrays keine spezielle Behandlung wie in den meisten C-ähnlichen Sprachen. Arrays sind normale Objekte und haben Methoden zum Zu-griff auf ihre Elemente. Lesender Zugriff auf einzelne Elemente erfolgt durch dieMethode apply, schreibender Zugriff durch update, also z.B.

scala> val array = new Array[String](2)array: Array[String] = Array(null, null)

scala> array.update(0,"World")

scala> array.update(1,"Reader")

scala> for (i <- 0 to 1)| println("Hello "+array.apply(i))

Hello WorldHello Reader

Es wird aber auch eine etwas eingängigere Syntax unterstützt. Auf die Elemen-te kann mit array(i) zugegriffen werden. Werte können in das Array mitarray(i) = <value> geschrieben werden. Das heißt, die obige Sitzung könn-te auch folgendermaßen aussehen:

scala> val array = new Array[String](2)array: Array[String] = Array(null, null)

scala> array(0) = "World"scala> array(1) = "Reader"

Page 73: Scala : objektfunktionale Programmierung

60 4 Reine Objektorientierung

scala> for (i <- 0 to 1)| println("Hello "+array(i))

Hello WorldHello Reader

Dies ist aber auch keine Sonderbehandlung von Arrays, sondern funktioniertgrundsätzlich für jedes beliebige Objekt.Die Regel für apply lautet: Folgen einem Objektnamen10 Klammern mit einemoder mehreren Werten, transformiert der Compiler dies in einen Aufruf der Me-thode apply mit den Werten in der Argumentliste. Es reicht also, in einer Klasseoder einem Objekt die passende Methode apply zu definieren, um diese Schreib-weise nutzen zu können.Die Regel für update lautet: Wird einer Variablen, der Klammern mit einem odermehreren Argumenten folgen, ein Wert zugewiesen, transformiert der Compilerdies in einen Aufruf der Methode updatemit den Argumenten, die der Variablenfolgen, und dem Wert auf der rechten Seite des Zuweisungsoperators. Das heißt,auch für diese Notation reicht es die Methode update zu definieren.

4.1.2 Was Klassen sonst noch enthalten können

Neben Feldern und Methoden können Klassen auch Klassen, Objekte und Typ-synonyme enthalten. Dasselbe gilt natürlich auch für Objekte. Beginnen wir mitden Typsynonymen. Mit dem Schlüsselwort type kann einem bestehenden Typein neuer Name gegeben werden. Wenden wir uns noch einmal der Hello-Klassezu. In Listing 4.9 wird ein Typsynonym Greeting definiert.

Listing 4.9: Die Klasse Hello mit dem Typsynonym Greeting

class Hello {type Greeting = Stringvar greeting: Greeting = "Hello"def hello() {println(greeting)

}}

Erzeugen wir anschließend ein Objekt say vom Typ Hello, hat das Feld greet-ing den Typ say.Greeting, wie im Folgenden zu sehen ist:

scala> val say = new Hellosay: Hello = Hello@1497b7b1

scala> say.greetingres0: say.Greeting = Hello

10 Ein Objektname ist der Name eines Singleton-Objekts oder der Bezeichner einer var oder val, dieein Objekt referenziert.

Page 74: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 61

Wollen wir zu einer Klasse oder einem Objekt eine innere Klasse oder ein Objektdefinieren, müssen wir es nur innerhalb der geschweiften Klammern definieren.Ein Beispiel zur Verdeutlichung ist in Listing 4.10 angegeben.

Listing 4.10: Verschachtelte Klassen und Objekte

class A {object B {val c = 5

}class D {object E {val c = 7

}val c = 9

}val f = new D

}

Genutzt werden könnte die Klasse A aus Listing 4.10 dann beispielsweise folgen-dermaßen:

scala> val x = new Ax: A = A@1c7b0f4d

scala> x.B.cres1: Int = 5

scala> x.f.cres2: Int = 9

scala> x.f.E.cres3: Int = 7

Das Verschachteln von Klassen und Objekten macht üblicherweise dann Sinn,wenn eine Klasse oder ein Objekt mehrere Member enthält, die sich gut zusam-menfassen lassen, aber außerhalb nicht benötigt werden. Ein Beispiel ist in Listing4.11 angegeben.

Listing 4.11: Die Klasse Customer, die ein Objekt Address enthält

class Customer {var customerID: Int = _object Address {var street: String = _var postcode: String = _var city: String = _var country: String = _

}}

Page 75: Scala : objektfunktionale Programmierung

62 4 Reine Objektorientierung

Die Klasse Customer enthält ein Objekt Address, in dem die Adresse des Kun-den gekapselt ist. Im folgenden Abschnitt lernen wir nun endlich die Konstrukto-ren kennen, mit denen wir die Felder beim Erzeugen des Objektes gleich sinnvol-ler belegen können.

4.1.3 Konstruktoren

Neue Objekte werden mit dem Schlüsselwort new konstruiert, das vor den Klas-sennamen geschrieben wird, z.B. new Hello. Es kann natürlich auch wie bei-spielsweise in Java üblich new Hello() geschrieben werden. Beides ist in Scalaäquivalent und hat in diesem Fall natürlich nichts mit dem Uniform Access Princi-ple zu tun. Üblicherweise werden in Scala zugunsten von knapperem und besserlesbarem Code die Klammern weggelassen.Wird ein Objekt mit new erzeugt, so wird dazu ein Konstruktor ausgeführt. Bis-her haben wir keinen Konstruktor angegeben, sondern uns auf den automatischgenerierten Konstruktor gestützt.Im Fall der Hello-Klasse wollen wir nun die Möglichkeit schaffen direkt beimErzeugen eines Hello-Objektes einen eigenen Begrüßungstext zu übergeben. InScala funktioniert das mit sogenannten Klassen-Parametern, die direkt nach demKlassennamen angegeben werden.

Listing 4.12: Die Klasse Hello mit dem Klassen-Parameter greeting

class Hello(greeting: String) {def hello() {println(greeting)

}}

Listing 4.12 definiert die Hello-Klasse mit dem Klassen-Parameter greeting.Wird nun ein Objekt mit new Hello("Hello") erzeugt, verhält es sich genausowie das zuvor mit der Klasse aus Listing 4.2 und new Hello erzeugte Objekt. Derwesentliche Unterschied ist, dass auf greeting nicht außerhalb der Klasse zuge-griffen werden kann, da mit dem Klassen-Parameter nicht automatisch ein Feldgreeting definiert wurde. Scala erzeugt aus den Klassen-Parametern automa-tisch einen Konstruktor mit genau diesen Parametern, der als primary constructorbezeichnet wird. Ohne Parameter, also mit new Hello, kann jetzt kein Objektmehr konstruiert werden.Soll zu einem Klassen-Parameter automatisch ein Feld mit demselben Namen er-zeugt werden, muss der Parameter als val oder var deklariert werden. Es kön-nen zusätzlich noch Sichtbarkeitsmodifier vorangestellt werden.

Page 76: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 63

Listing 4.13: Die Klasse Hellomit dem Klassen-Parameter greeting und Feld greeting

class Hello(var greeting: String) {def hello() {println(greeting)

}}

Wird die Klasse Hello wie in Listing 4.13 angegeben definiert, kann in einemHello-Objekt nach dem Erzeugen der Begrüßungstext wie zuvor geändert wer-den, z.B.

scala> val say = new Hello("Hello")say: Hello = Hello@63d12a6

scala> say.hello()Hello

scala> say.greeting = "Hi"

scala> say.hello()Hi

Alles, was der primäre Konstruktor darüber hinaus noch tun soll, wird unmittel-bar in die Klasse geschrieben. Beispielsweise kann mit der Methode require11

überprüft werden, ob eine Bedingung erfüllt ist. In Listing 4.14 wird beim Er-zeugen eines Objekts überprüft, dass der übergebene String nicht leer ist. DasAusrufungszeichen steht dabei für die logische Negation, also „der übergebeneString ist leer, gilt nicht”.

Listing 4.14: Die Klasse Hello mit der Überprüfung des Klassen-Parameters

class Hello(greeting: String) {require(!greeting.isEmpty)def hello() {println(greeting)

}}

Oft ist es erwünscht, zum Erzeugen von Objekten mehr als einen Konstruktorzur Verfügung zu stellen, um Objekte mit unterschiedlichen Parameterlisten er-zeugen zu können. Die weiteren Konstruktoren werden als auxiliary constructorsbezeichnet und mittels def this(...) definiert. Jeder zusätzliche Konstruk-tor muss als Erstes einen anderen Konstruktor mit this(...) aufrufen. Ziel istes, damit sicherzustellen, dass beim Erzeugen eines Objektes die Anweisungendes primären Konstruktors vor den Anweisungen der zusätzlichen Konstrukto-

11 require ist eine ganz normale Methode, die im Objekt Predef definiert ist und an damit an belie-biger Stelle genutzt werden kann. Sie wirft eine IllegalArgumentException, wenn das Argumentzu false ausgewertet wird.

Page 77: Scala : objektfunktionale Programmierung

64 4 Reine Objektorientierung

ren ausgeführt werden und es somit einen einzigen Einstiegspunkt in die Klassegibt.

Listing 4.15: Die Klasse Hello mit einem zusätzlichen Konstruktor

class Hello(greeting: String) {require(!greeting.isEmpty)def this() = this("Hello")def hello() {println(greeting)

}}

Listing 4.15 definiert zusätzlich zum primären einen parameterlosen Konstruktor,der den Begrüßungstext auf den Wert Hello setzt. Ein mit new Hello erzeugtesObjekt antwortet auf hello dann mit Hello. Das leere Klammerpaar darf beider Definition des Konstruktors nicht weggelassen werden. Wird ein zusätzlicherKonstruktor aber nur benötigt, um einen Standardwert zu setzen, kann dies seitScala 2.8.0 auch direkt durch ein Default-Argument realisiert werden.

Listing 4.16: Die Klasse Hello mit Klassen-Parameter und Default-Argument

class Hello (greeting: String = "Hello") {require(!greeting.isEmpty)def hello() {println(greeting)

}}

Wird die Klasse Hello wie in Listing 4.16 angegeben definiert, so können an-schließend Objekte mit new Hello("Hi"), aber auch mit new Hello erzeugtwerden, ohne dass es einen zusätzlichen Konstruktor gibt. Der primäre Konstruk-tor kann aufgrund des Default-Arguments auch mit einer leeren Argumentlisteaufgerufen werden.Scala bietet darüber hinaus noch die Möglichkeit, Objekte ohne new zu er-zeugen. Zum Beispiel erzeugt List(1,2,3) eine Liste mit den Elementen1, 2 und 3. Dies ist keine Besonderheit für Klassen der Standardbibliothek,sondern kann auf einfache Weise auch für eigene Klassen adaptiert werden.Der Ausdruck List(1,2,3) wird nämlich durch den Compiler expandiertzu List.apply(1,2,3). Das heißt, apply ist eine Methode des Companion-Objektes. Wird die Methode dazu genutzt, ein Objekt der dazugehörigen Klassezu erzeugen und zurückzugeben, so nennen wir dies Factory-Methode.

Listing 4.17: Die Klasse Hello mit Factory-Methoden im Companion-Objekt

class Hello (greeting: String = "Hello") {require(!greeting.isEmpty)

Page 78: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 65

def hello() {println(greeting)

}}object Hello {def apply() = new Hellodef apply(greeting: String) = new Hello(greeting)

}

Wird die Klasse Hello mit den beiden apply-Methoden im Companion-Objektwie in Listing 4.17 dargestellt definiert, können anschließend Hello-Objekte so-wohl mit Hello() als auch mit Hello(<greeting>) erzeugt werden. Würdedas leere Klammerpaar im ersten Fall weggelassen, so würde der Ausdruck dasCompanion-Objekt selbst bezeichnen, d.h. nach Auswertung der Zeile

val say = Hello

ist say kein Hello-Objekt, sondern eine Referenz auf das Singleton-ObjektHello. Daher versteht say die Nachricht hello nicht. Stattdessen kann dannaber mit dem Ausdruck say() ein Hello-Objekt erzeugt werden, denn say()wird transformiert zu say.apply().Betrachten wir nun noch einmal die Erweiterung des Application-Traits, um ein-fache Programme ausführbar zu machen. Wie in Abschnitt 3.3 auf Seite 43 darge-stellt, wird der gesamte Code, der direkt im definierten Objekt und nicht innerhalbeines Members steht, beim Starten ausgeführt. Dies ist ein grundsätzliches Featurevon Scala, da dieser Code ja in den primären Konstruktor aufgenommen und beiInitialisierung eines Objektes ausgeführt wird. Das Starten eines Objektes initia-lisiert es als Erstes. Anschließend wird die main-Funktion des Application-Traitsausgeführt und das Programm beendet.

4.1.4 Enumerations

Bisher haben wir den Begrüßungstext als String repräsentiert. Damit habenwir eine sehr flexible Implementierung gewählt, die aber beliebige Werte zu-lässt. So kann beispielsweise ein Nutzer der Hello-Klasse mit new Hello("Tankstelle") ein Hello-Objekt erzeugen, das auf die Nachricht hello mitTankstelle antwortet.Eine einfache Möglichkeit wäre es, den Klassen-Parameter mit einer Liste zuläs-siger Werte zu vergleichen, wie in Listing 4.18 dargestellt. Im Companion-Objektwird eine Liste zulässiger Werte für greeting definiert und im primären Kon-struktor als Argument von require überprüft.

Listing 4.18: Vergleich des Klassen-Parameters mit einer Liste zulässiger Werte

class Hello(greeting: String = "Hello") {require(Hello.acceptableGreetings contains greeting)

}

Page 79: Scala : objektfunktionale Programmierung

66 4 Reine Objektorientierung

object Hello {private val acceptableGreetings =List("Hello","Hi","Howdy")

}

Wird nun in einer Applikation ein Hello-Objekt mit einem nicht zulässigen Werterzeugt, z.B. new Hello("Salve"), kann der Quelltext ohne Fehlermeldungübersetzt werden12. Erst beim Ausführen des Programms meldet sich die Lauf-zeitumgebung mit

java.lang.ExceptionInInitializerErrorat HelloTest.main(HelloRuntimeError.scala)...

Caused by: java.lang.IllegalArgumentException:requirement failed

at scala.Predef$.require(Predef.scala:135)...

Besser wäre es also, die zulässigen Werte gleich beim Übersetzen überprüfen zukönnen. Viele Programmiersprachen bieten dafür sogenannte Aufzählungstypenoder auf Englisch Enumerations, mit denen eine endliche Anzahl von Werten fest-gelegt werden kann. Auch in Scala können Aufzählungstypen definiert werden,allerdings ganz nach der Scala-Philosophie auch wieder nicht in spezieller Sprach-syntax, sondern als ganz normale Objekte.

Listing 4.19: Enumeration für Begrüßungstexte

object Greeting extends Enumeration {val Hello, Hi, Howdy = Value

}

In Listing 4.19 wird das Objekt Greeting für einen Aufzählungstypen defi-niert. Dazu wird die Klasse Enumeration erweitert. Die Werte haben den TypGreeting.Value. Jeder Aufruf der Methode Value erzeugt einen neuen Wert,der Teil der Enumeration wird. Überladene Versionen der Methode Value ermög-lichen es, eine Integer- bzw. eine spezielle String-Repräsentation des Wertes anzu-geben, z.B. könnte der Default-Wert des Begrüßungstextes durch val Default= Value("Hello") definiert werden.

Listing 4.20: Klasse Hello mit Enumeration für Begrüßungstexte

class Hello(greeting: Greeting.Value = Greeting.Hello) {def hello() {println(greeting)

}}

12 Achtung: Aufgrund des private-Modifiers im Companion-Objekt und dem Zugriff aus der Klassekann dieser Code nicht in der Scala-Shell eingegeben werden.

Page 80: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 67

Soll die Klasse Hello nun den Aufzählungstyp nutzen, bekommt der Klassen-Parameter greeting den entsprechenden Typ Greeting.Value und der De-fault-Wert wird auf Greeting.Hello gesetzt, siehe Listing 4.20. Durch Defini-tion eines Typsynonyms, den Default-Wert und ein Import-Statement13 vor derHello-Klasse wie in Listing 4.21 wird der Quellcode noch etwas besser lesbar.

Listing 4.21: Klasse Hello mit Enumeration für Begrüßungstexte, Typsynonym undDefault-Wert

object Greeting extends Enumeration {type Greeting = Valueval Default = Value("Hello")val Hi, Howdy = Value

}import Greeting._class Hello(greeting: Greeting = Default) {def hello() {println(greeting)

}}

Durch type Greeting = Value wird ein Typsynonym definiert. Das heißt,der Typ Greeting.Value hat nun auch den Namen Greeting.Greeting.Nach dem Import kann dann jeweils das Präfix Greeting weggelassen wer-den. Wird nun beispielsweise versucht, Programmcode zu übersetzen, der newHello(Huhu) enthält, so schlägt dies mit folgender Meldung fehl:

error: not found: value Huhu

4.1.5 Vererbung und Subtyping

Ein weiteres wesentliches Merkmal der objektorientierten Programmierung ist dieAbleitung einer Klasse von einer anderen. Die abgeleitete Klasse wird als Sub-klasse, die Klasse, von der abgeleitet wurde, als Basisklasse oder auch Superklassebezeichnet. Beim Ableiten erbt die Subklasse alle nicht-privaten Member der Ba-sisklasse, d.h. die Member sind Bestandteil der Subklasse, als wären sie erst darindefiniert worden.Bisher haben wir bei unseren Klassendefinitionen keine Basisklasse angegeben.Dadurch ist die Basisklasse automatisch die Klasse scala.AnyRef, die auf derJava-Plattform der Klasse java.lang.Object entspricht. Die in AnyRef defi-nierten Methoden sind daher auch in allen Klassen verfügbar, z.B. können zweiHello-Objekte mit der Methode == verglichen werden, ohne dass diese Methodeexplizit in der Klasse Hello definiert worden ist.

13 Importe werden in Abschnitt 4.2 erklärt. An dieser Stelle reicht es zu wissen, dass damit die Mem-ber des importierten Objektes, z.B. Greeting, nicht mehr mit dem voll qualifizierten Namen, z.B.Greeting.Hello, angesprochen werden müssen. Es reicht dann zu schreiben Hello.

Page 81: Scala : objektfunktionale Programmierung

68 4 Reine Objektorientierung

Soll von einer anderen Klasse geerbt werden, muss dies mit dem Schlüsselwortextends bei der Definition einer Klasse angegeben werden.

Listing 4.22: Klasse HelloAndGoodbye als Subklasse von Hello

class HelloAndGoodbye extends Hello {def goodbye() = println("Goodbye")

}

Die in Listing 4.22 definierte Klasse HelloAndGoodbye erbt die Methode helloaus der Klasse Hello. Daher versteht ein HelloAndGoodbye-Objekt sowohl dieNachricht goodbye als auch hello. Die folgende Sitzung veranschaulicht dies.

scala> val say = new HelloAndGoodbyesay: HelloAndGoodbye = HelloAndGoodbye@4e836869

scala> say.hello()Hello

scala> say.goodbye()Goodbye

Beim Verwenden der Methoden sehen wir keinen Unterschied, ob die Methode inder Klasse definiert oder von einer Basisklasse geerbt wurde.Nachdem jede Klasse auch einen Datentypen definiert, stellt sich die Frage, obdie Datentypen der Sub- und Basisklasse in irgendeiner Weise zusammenhängen.Tatsächlich ist es so, dass der Typ der Subklasse ein sogenannter Subtyp des Typsder Basisklasse ist. Die Subklasse ist eine Spezialisierung der Basisklasse, und um-gekehrt sprechen wir von einer Generalisierung.Nachdem die Klasse Hello einen Klassen-Parameter zum Setzen des Begrü-ßungstextes hat, wollen wir diesen auch in der Klasse HelloAndGoodbye nutzenund HelloAndGoodbye selbst um zwei Klassen-Parameter erweitern.

Listing 4.23: Klasse HelloAndGoodbye mit Klassen-Parametern

class HelloAndGoodbye(greeting: String = "Hi",farewell: String = "Goodbye"

) extends Hello(greeting) {def goodbye() {println(farewell)

}}

Listing 4.23 zeigt eine Implementierung der Klasse HelloAndGoodbye mit denzwei Klassen-Parametern greeting und goodbye inklusive Default-Argumen-ten. Der Klassen-Parameter greeting wird als Parameter an die BasisklasseHello übergeben. Dies entspricht dem Aufruf des Konstruktors der Basisklas-

Page 82: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 69

se, der in Java beispielsweise mit dem Schlüsselwort super erfolgt. In Scala kannder Aufruf des Konstruktors der Basisklasse ausschließlich auf die in Listing 4.23dargestellte Weise erfolgen.Das Schlüsselwort super gibt es in Scala aber dennoch, und zwar zum Aufrufeiner Methode der Basisklasse. Dies ist beispielsweise nützlich, wenn die geerbteMethode redefiniert wird und an einer Stelle doch die Methode der Basisklasseverwendet werden soll oder in Stackable Traits, die in Abschnitt 4.3.2 erklärt wer-den.Wird ein HelloAndGoodbye-Objekt ohne Argumente erzeugt, so wird der Be-grüßungstext auf den in HelloAndGoodbye angegebenen Default-Wert Hi ge-setzt. Der Default-Wert von greeting aus der Klasse Hello kommt, so wiedie beiden Klassen implementiert sind, nicht mehr zum Tragen, da immer derDefault-Wert von greeting aus der Definition von HelloAndGoodbye genutztwird. Wird dieser Default-Wert einfach weggelassen, ist es nicht mehr möglich,ein HelloAndGoodbye-Objekt ohne Argumente zu erzeugen, da der Aufrufdes Konstruktors in der Klasse HelloAndGoodbye nicht ohne das greeting-Argument aufgerufen werden kann.

Listing 4.24: Klassen-Parameter in Hello und HelloAndGoodbye nutzen den selbenDefault-Wert.

object Hello {val defaultGreeting = "Hello"

}class Hello(greeting: String = Hello.defaultGreeting) {def hello() {println(greeting)

}}class HelloAndGoodbye(

greeting: String = Hello.defaultGreeting,farewell: String = "Goodbye"

) extends Hello(greeting) {def goodbye() {println(farewell)

}}

Soll der Default-Wert aus Hello genutzt werden, kann dies beispielsweise wiein Listing 4.24 realisiert werden. Der Default-Wert wird dort im Feld default-Greeting des Objektes Hello gespeichert und kann so in den beiden KlassenHello und HelloAndGoodbye genutzt werden.

Listing 4.25: Fehlerhaftes Redefinieren eines Feldes

class Hello(var greeting: String) {def hello() {

Page 83: Scala : objektfunktionale Programmierung

70 4 Reine Objektorientierung

println(greeting)}

}class HelloAndGoodbye(var greeting: String, // does not compilevar farewell: String) extends Hello(greeting) {def goodbye() {println(farewell)

}}

Wenn Sie Felder aus den Klassen-Parametern erzeugen lassen, müssen Sie daraufachten, dass die Felder, solange sie nicht private sind, auch vererbt werden.Im Quelltext in Listing 4.25 wird das erzeugte Feld greeting von Hello durchdas Feld greeting in HelloAndGoodbye redefiniert. Nachdem dort aber einoverride stehen muss, schlägt das Kompilieren mit folgender Meldung fehl:

HelloAndGoodbye.scala:7: error: overriding variablegreeting in class Hello of type String;

variable greeting needs ‘override’ modifiervar greeting: String, // does not compile

^one error found

Das Redefinieren des Feldes ist in Listing 4.25 ohnehin überflüssig, da das Feldgreeting von Hello geerbt und damit sowieso schon verfügbar ist. Ein zu-gegebenermaßen etwas konstruierter Anwendungsfall läge beispielsweise in Lis-ting 4.26 vor. Dort wird zunächst eine Klasse HelloDefault definiert mit einemunveränderlichen Feld greeting, das nicht durch einen Klassen-Parameter ge-setzt werden kann. Um in der Subklasse Hello diesen Wert mit einem Klassen-Parameter setzen zu können, muss es dort redefiniert werden. Wird dies nicht ge-macht, wird auch ein mit new Hello("Hi") erzeugtes Objekt auf die Nachrichthello den String Hello ausgeben und nicht Hi.

Listing 4.26: Redefinieren eines Feldes mit einem Klassen-Parameter

class HelloDefault {val greeting = "Hello"def hello() {println(greeting)

}}class Hello(override val greeting: String)

extends HelloDefault

Listing 4.26 zeigt noch ein weiteres Merkmal von Scala: Wenn der Rumpf einerKlasse leer ist wie bei der Klasse Hello, können die geschweiften Klammern weg-gelassen werden.

Page 84: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 71

Übrigens kann das Redefinieren auch verhindert werden, indem das Member inder Basisklasse mit dem Modifier final versehen wird. Listing 4.27 zeigt ein Bei-spiel dafür.

Listing 4.27: Verhinderung der Redefinition eines Feldes durch den final-Modifier

class AlwaysHello {final val greeting = "Hello"def hello() {println(greeting)

}}

Der Versuch, greeting in einer abgeleiteten Klasse zu redefinieren, wird mit ei-ner Fehlermeldung quittiert, wie in der folgenden Sitzung zu sehen ist:

scala> class Hi extends AlwaysHello {| override val greeting = "Hi"| }

<console>:7: error: overriding value greeting in classAlwaysHello of type java.lang.String("Hello");

value greeting cannot override final memberoverride val greeting = "Hi"

Besteht zwischen zwei Klassen eine Vererbungsrelation, so kann ein Objekt derSubklasse einer Variablen vom Typ der Basisklasse zugewiesen werden. Es kannz.B. geschrieben werden:

scala> val sayG: Hello = new HelloAndGoodbye("Hi","CU")sayG: Hello = HelloAndGoodbye@1c0693a5

Obwohl das erzeugte Objekt die Nachricht goodbye verstehen würde, kann dieMethode goodbye über die Variable sayG nicht ausgeführt werden:

scala> sayG.goodbye()<console>:9: error: value goodbye is not a member of

HellosayG.goodbye

Der Aufruf der Methode hello funktioniert aber erwartungsgemäß:

scala> sayG.hello()Hi

Betrachten wir nun eine weitere von Hello abgeleitete Klasse HelloTwice, diein Listing 4.28 dargestellt ist.

Listing 4.28: Von der Klasse Hello abgeleitete Klasse HelloTwice

class HelloTwice(greeting: String)extends Hello(greeting) {

Page 85: Scala : objektfunktionale Programmierung

72 4 Reine Objektorientierung

override def hello() {println(greeting+", "+greeting)

}}

In der Klasse HelloTwice wird die von Hello geerbte Methode hello rede-finiert. Statt den Begrüßungstext einmal auszugeben, wird er nun zweimal miteinem Komma getrennt ausgegeben. Erzeugen wir nun ein HelloTwice-Objektund weisen es einer Hello-Variablen zu, passiert beim Aufruf von hello Fol-gendes:

scala> val sayT: Hello = new HelloTwice("Salut")sayT: Hello = HelloTwice@65c94b8f

scala> sayT.hello()Salut, Salut

Obwohl sayT den Typ Hello hat, wird die Methode hello aus HelloTwiceausgeführt und Salut, Salut ausgegeben. Diese Phänomene lassen sich durchPolymorphie und dynamisches Binden erklären.Die Variable sayT ist vom Typ Hello. Als solches kann sie ein Objekt referen-zieren, das den Typ Hello hat. Dies beinhaltet auch Subtypen, da es sich dabeium eine sogenannte Spezialisierung handelt, das heißt ein HelloTwice-Objekt istein spezielles Hello-Objekt. Die Variable sayT ist also polymorph, zu deutschvielgestaltig. Das Gleiche gilt natürlich auch für die Variable sayG.Beim Aufruf von Methoden der Variablen sayG und sayT kommen für den Com-piler nun aber nur Methoden in Betracht, die in der Klasse Hello definiert wor-den sind. Daher schlägt der Versuch say.goodbye() zu nutzen, bereits beimKompilieren fehl. In der Klasse Hello gibt es keine Methode goodbye.Um zu verstehen, dass sayT.hello() nun aber die Methode aus HelloTwicenimmt, hilft es, den Methodenaufruf noch einmal formal als Senden von Nach-richten zu betrachten. Wird an die Variable sayT die Nachricht hello gesen-det, überprüft der Compiler, ob ein Objekt, das durch sayT referenziert wird, dieNachricht überhaupt verstehen könnte. Nachdem sayT den Typ Hello hat unddie Klasse Hello die Methode hello enthält, ist dies zulässig.Zur Laufzeit wird nun tatsächlich die Nachricht an das von sayT referenzierteObjekt gesendet. Das HelloTwice-Objekt reagiert darauf mit der Ausführungseiner hello-Methode. Das heißt also, die Zulässigkeit des Methodenaufrufswird statisch geprüft, aber der tatsächliche Methodenaufruf wird dynamisch ge-bunden.In abgeleiteten Klassen können nicht nur Methoden, sondern grundsätzlich allevon der Basisklasse geerbten Member redefiniert werden. Dazu ist jedes Mal deroverride-Modifier der neuen Definition voranzustellen.Auf Seite 58 haben wir bereits das Uniform Access Principle erläutert, nachdem esfür die Verwendung einer Klasse transparent sein muss, ob ein Attribut als Feld

Page 86: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 73

oder Methode implementiert wird. Dieser Ansatz wird auch auf die Vererbungausgeweitet. Enthält eine Klasse eine parameterlose Methode, kann diese durchein val redefiniert werden. Interessant ist dies insbesondere im Zusammenhangmit abstrakten Klassen, die im nächsten Abschnitt besprochen werden.

4.1.6 Abstrakte Klassen

Eine Klasse heißt abstrakt, wenn sie mindestens ein Member enthält, das abstraktist. Ein Member heißt abstrakt, wenn es keine vollständige Definition hat. Ent-hält eine Klasse kein abstraktes Member, heißt sie konkret. Dieses in den meistenobjektorientierten Programmiersprachen enthaltene Konzept legt das Vorhanden-sein eines Members in einer Klasse fest und fordert damit von allen konkretenSubklassen, dass es dort implementiert wird. Zur Verdeutlichung wählen wir einin vielen Programmierbüchern verwendetes Beispiel. Wir wollen Hunde, Katzenund Kühe modellieren. Ein erster Ansatz wäre die direkte und unabhängige De-finition der entsprechenden Klassen, siehe Listing 4.29.

Listing 4.29: Die Klassen Dog, Cat und Cow

class Dog {def makeNoise() {println("woof, woof")

}}class Cat {def makeNoise() {println("miaow")

}}class Cow {def makeNoise() {println("moo")

}}

Mit den Definitionen der Klassen Dog, Cat und Cow können nun Hunde, Katzenund Kühe erzeugt werden und mit makeNoise ein Geräusch machen:

scala> val lassie = new Doglassie: Dog = Dog@1b9f9088

scala> lassie.makeNoise()woof, woof

scala> val mauzi = new Catmauzi: Cat = Cat@25d3e3f3

scala> mauzi.makeNoise()miaow

Page 87: Scala : objektfunktionale Programmierung

74 4 Reine Objektorientierung

scala> val emma = new Cowemma: Cow = Cow@645b56c4

scala> emma.makeNoise()moo

Obwohl jedes Tier eine makeNoise-Methode hat, ist es nicht möglich, verschie-dene Tiere einfach in eine Liste zu speichern und mit einer for-Schleife an jedesdie Nachricht makeNoise zu senden:

scala> val animals = List(lassie, mauzi, emma)animals: List[ScalaObject] = List(Dog@1b9f9088,

Cat@25d3e3f3, Cow@645b56c4)

scala> for (animal <- animals) animal.makeNoise()<console>:10: error: value makeNoise is not a member of

ScalaObjectanimal.makeNoise()

Um das Problem zu verstehen, werfen wir einen Blick auf die Typen: lassie hatden Typ Dog, mauzi hat den Typ Cat und emma hat den Typ Cow. Fügen wir diedrei in eine Liste ein, so kann der Typparameter des Listentyps ermittelt werden.Der gesuchte Typ muss Supertyp von Dog, Cat und Cow sein. Der Compiler infe-riert als speziellsten Supertypen ScalaObject und legt damit den Typ der Listeanimals als List[ScalaObject] fest. Damit haben gleichzeitig alle Elementeder Liste, wenn sie über die Liste referenziert werden, den Typ ScalaObject.Nun überprüft der Compiler, ob der Methodenaufruf makeNoise in der for-Schleife für ein ScalaObject zulässig ist. Nachdem aber makeNoise nicht inScalaObject vorhanden ist, schlägt das Übersetzen des Programmstücks fehl.Um aber doch mit der Liste und makeNoise arbeiten zu können, müssen wir alsoeine gemeinsame Basisklasse einführen, die die makeNoise-Methode enthält. Dernächste Ansatz wäre also der in Listing 4.30 dargestellte.

Listing 4.30: Die Klasse Animal und davon abgeleitete Klassen Dog, Cat und Cow

class Animal {def makeNoise() {println("?")

}}class Dog extends Animal {override def makeNoise() {println("woof, woof")

}}class Cat extends Animal {override def makeNoise() {println("miaow")

}

Page 88: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 75

}class Cow extends Animal {override def makeNoise() {println("moo")

}}

Mit dem Ansatz in Listing 4.30 funktioniert nun unsere for-Schleife, aber es gibtnoch zwei Dinge, die wir lieber nicht gehabt hätten. Erstens können nun Animal-Objekte erzeugt werden. Wir wollen aber eigentlich nur Hunde, Katzen und Kühezulassen. Zweitens mussten wir die Methode makeNoise in der Klasse Animalimplementieren, wissen aber gar nicht so richtig was wir dort ausgeben sollen.Beide Probleme bekommen wir mit der in Listing 4.31 dargestellten Lösung inden Griff.

Listing 4.31: Die abstrakte Klasse Animal und davon abgeleitete Klassen Dog, Cat undCow

abstract class Animal {def makeNoise(): Unit

}class Dog extends Animal {def makeNoise() {println("woof, woof")

}}class Cat extends Animal {def makeNoise() {println("miaow")

}}class Cow extends Animal {def makeNoise() {println("moo")

}}

Die in Listing 4.31 implementierte Klasse Animal ist abstrakt, weil sie eine Me-thode ohne Implementierung hat. Zusätzlich muss vor dem Schlüsselwort classnoch das Schlüsselwort abstract stehen, das die Klasse als abstrakte Klasse mar-kiert. Würde es nicht dort stehen, würde ein Übersetzungsversuch der Klasse miteiner Fehlermeldung abgebrochen:

Animal.scala:1: class Animal needs to be abstract, sincemethod makeNoise is not defined

class Animal {^

In den Klassen Dog, Cat und Cow wird die Methode makeNoise nun nicht mehrredefiniert, sondern erstmalig implementiert. Daher kann der Modifier override

Page 89: Scala : objektfunktionale Programmierung

76 4 Reine Objektorientierung

entfallen. Er darf an dieser Stelle aber auch stehen bleiben. Wie die folgende Sit-zung zeigt, können nun ein Hund, eine Katze und eine Kuh zusammen in einerListe sein und mithilfe einer for-Schleife ein Geräusch machen:

scala> for (animal <- List(new Dog, new Cat, new Cow))| animal.makeNoise()

woof, woofmiaowmoo

Abstrakte Klassen können neben abstrakten Methoden auch abstrakte vars, valsund Typen, aber auch konkrete Member enthalten. Eine erweiterte Animal-Klassekönnte wie in Listing 4.32 aussehen.

Listing 4.32: Die abstrakte Klasse Animal

import java.util.Calendarabstract class Animal(val dateOfBirth: Calendar) {var name: Stringdef makeNoise(): Unitdef age = {val today = Calendar.getInstanceval age = today.get(Calendar.YEAR) -

dateOfBirth.get(Calendar.YEAR)today.set(Calendar.YEAR,

dateOfBirth.get(Calendar.YEAR))if (today before dateOfBirth)age-1

elseage

}override def toString =name+" "+age+" year"+(if (age!=1) "s" else "")+" old"

}

Die Klasse Animal definiert ein abstraktes Feld name14, einen Klassen-ParameterdateOfBirth, der als val auch von außen nutzbar ist, eine abstrakte Metho-de makeNoise und eine konkrete Methode age. Außerdem wird die MethodetoString redefiniert.Nachdem wir für das Geburtsdatum und die Berechnung des Alters die Java-Klasse Calendar aus dem Package java.util benutzen, aber nicht an jederStelle ausführlich java.util.Calendar schreiben wollen, importieren wir mitder ersten Zeile die Klasse und können fortan einfach Calendar schreiben.Eine konkrete Subklasse von Animal muss dann name und makeNoise im-plementieren und den Klassen-Parameter dateOfBirth setzen. Wird eines der

14 Wir unterstützen damit die Möglichkeit, dass kleine Kinder ihr Haustier täglich umbenennen kön-nen.

Page 90: Scala : objektfunktionale Programmierung

4.1 Klassen und Objekte 77

Member nicht implementiert, ist auch die abgeleitete Klasse abstrakt. In Listing4.33 ist die konkrete Klasse Dog dargestellt.

Listing 4.33: Die Klasse Dog

import java.util.Calendarclass Dog(

var name: String,dateOfBirth: Calendar = Calendar.getInstance // today) extends Animal(dateOfBirth) {

def makeNoise() {println("woof, woof")

}}

Die folgende Sitzung zeigt die Verwendung der Klasse Dog und die Implementie-rung von trixi, einem Vogel:

scala> import java.util.{Calendar,GregorianCalendar}import java.util.{Calendar, GregorianCalendar}

scala> val lassie = new Dog("Lassie",| new GregorianCalendar(1999,Calendar.APRIL,12))

lassie: Dog = Lassie 11 years old

scala> val hugo = new Dog("Hugo")hugo: Dog = Hugo 0 years old

scala> val trixi = new Animal(Calendar.getInstance) {| var name = "Trixi"| def makeNoise() {| println("chirp, chirp")| }| }

trixi: Animal = Trixi 0 years old

scala> trixi.makeNoise()chirp, chirp

Auch in der Scala-Shell ist es möglich, ein Import-Statement zu verwenden. Mitdem angegebenen Statement werden Calendar und GregorianCalendar ausjava.util importiert und können anschließend ohne das Präfix java.util ge-nutzt werden. Importe werden ausführlich in Abschnitt 4.2 besprochen.Durch die Definition von trixi entsteht der Eindruck, die Klasse Animal könn-te doch instanziiert werden, zumal die Scala-Shell auch noch den Typ Animalfür trixi inferiert. Dies ist aber nicht der Fall. Durch new Animal(...){...}wird eine anonyme Klasse definiert, die Animal erweitert. Nachdem die Klassekeinen eigenen Namen hat, definiert sie auch keinen neuen Typ. Daher bekommttrixi den Typ Animal.

Page 91: Scala : objektfunktionale Programmierung

78 4 Reine Objektorientierung

4.2 Codeorganisation

Spätestens sobald wir mit mehreren Klassen und Vererbung arbeiten, ist es wich-tig, den Code möglichst übersichtlich zu organisieren. In Scala lässt sich der Quell-code in Packages gruppieren. Nachdem wir bisher kein Package explizit angege-ben haben, befand sich alles im unbenannten Package.Das Definieren benannter Packages wird in Abschnitt 4.2.1 erklärt. Eine Neuerungin Scala 2.8 stellen die in Abschnitt 4.2.2 besprochenen Package Objects dar, mitdenen auch andere Elemente außer Klassen und Objekten zu Packages hinzuge-fügt werden können. Um nicht immer den voll qualifizierten Namen einer Klasseoder eines Objektes schreiben zu müssen, gibt es auch in Scala Import-Klauseln(siehe Abschnitt 4.2.3), mit denen auch noch einiges mehr möglich ist.

4.2.1 Packages

Wird am Anfang einer Datei eine Package-Klausel angegeben, befindet sich der ge-samte Quellcode dieser Datei in dem angegebenen Package. Eine Package-Klauselbeginnt mit dem Schlüsselwort package, gefolgt vom Package-Namen. Packageskönnen hierarchisch angeordnet werden. Die Trennung der Ebenen erfolgt mit ei-nem Punkt. Beispielsweise wird durch die Angabe der Klausel

package org.obraun.scalabook

der gesamte Quellcode der Datei dem Package scalabook im Package obraunim Package org zugeordnet. In Scala kann dies auch durch drei aufeinander fol-gende Package-Klauseln angegeben werden:

package orgpackage obraunpackage scalabook

Wie diese Schreibweise schon erahnen lässt, können in Scala Packages auch an-dere Packages enthalten. Daher muss das, was sich in einem äußeren Packa-ge befindet, nicht mit dem voll qualifizierenden Package-Namen angesprochenwerden muss. Befinden wir uns beispielsweise in einer Klasse innerhalb desPackages scalabook, so können wir die Klasse org.obraun.misc.Other mitmisc.Other referenzieren.Seit Scala 2.8 gibt es aber in den beiden oben angegebenen Schreibweisen einenUnterschied: Wenn die Packages wie im ersten Fall direkt mit einem Punkt ge-trennt zusammengeschrieben werden, wird nicht in den Zwischen-Packages ge-sucht, sondern nur Top-Level. Werden die Packages einzeln mit jeweils einerPackage-Klausel angegeben, wird auch dazwischen gesucht, was vor 2.8 in bei-den Versionen der Fall war.Um vor der Version 2.8 nur Top-Level zu suchen, konnte die Zeichenkette _root_bei einem Import vor das oberste Package gestellt werden. Selbstverständlich las-

Page 92: Scala : objektfunktionale Programmierung

4.2 Codeorganisation 79

sen sich die beiden Ansätze beliebig kombinieren. Beispielsweise wird mit denKlauseln

package orgpackage obraun.scalabook

zuerst im Package org, dann Top-Level gesucht.Scala-Packages können aber nicht nur für eine gesamte Quellcode-Datei festge-legt werden, sondern auch für Teile davon. Dazu ist es möglich, das, was zu ei-nem Package gehören soll, nach der Package-Klausel in geschweifte Klammerneinzufassen. Das macht es möglich, verschiedene Teile einer Datei verschiedenenPackages zuzuordnen. In Listing 4.34 ist ein Beispiel dargestellt.

Listing 4.34: Quellcode-Datei mit Code für mehrere Packages

package org {class Orgpackage obraun {class OrgObraun

}package scala {class OrgScala

}}package org.obraun.scalabook {object OrgObraunScalabook

}

Nach dem Übersetzen der in Listing 4.34 definierten Quellcode-Datei finden wirdie folgenden Verzeichnisse und Class-Dateien in der dargestellten Struktur vor:

org+-- obraun| +-- OrgObraun.class| +-- scalabook| +-- OrgObraunScalabook.class| +-- OrgObraunScalabook$.class+-- Org.class+-- scala

+-- OrgScala.class

4.2.2 Package Objects

Mit Scala 2.8 wurde unter anderem das Collection-Framework (siehe Abschnitt6.2) neu organisiert. Dadurch wanderten Klassen von einem Package in ein an-deres. Um eine möglichst sanfte Migration für bestehende Scala-Software zu ge-währleisten, liegt die Idee nahe, in den Packages, aus denen die Klasse ver-schwunden ist, einfach ein Typsynonym auf die neue Klasse und einen Wert,

Page 93: Scala : objektfunktionale Programmierung

80 4 Reine Objektorientierung

der das neue Companion-Objekt referenziert, zu definieren. Beispielsweise ist dieList aus dem Package scala in das Package scala.collection.immutableverschoben worden. Daher müsste in das Package scala nur Folgendes einge-fügt werden, um die Liste nach wie vor als scala.List zur Verfügung zu stel-len:

type List[+A] = scala.collection.immutable.List[A]val List = scala.collection.immutable.List

Leider funktioniert das so einfach nicht, denn obwohl in Scala verschiedene Kon-strukte fast beliebig ineinander verschachtelt werden können, dürfen Packagesnur Klassen und Objekte, aber keine Typsynonyme und Werte enthalten. Die Lö-sung fand sich mit den sogenannten package objects.Ein Objekt wird zu einem Package-Objekt, indem zum einen das Schlüssel-wort package dem Schlüsselwort object vorangestellt wird und es in einerQuellcode-Datei mit dem Namen package.scala in dem Verzeichnis des ent-sprechenden Packages abgespeichert wird. Alles, was in dem Package-Objekt de-finiert ist, wird Teil des entsprechenden Packages.Das heißt, im oben angegebenen Beispiel mit der Liste müssen die beiden Zeilenim Package-Objekt scala in der Datei package.scala im Verzeichnis scalaenthalten sein (siehe Listing 4.35).

Listing 4.35: Das Package-Objekt scala

package object scala {type List[+A] = scala.collection.immutable.List[A]val List = scala.collection.immutable.List// lots more

}

4.2.3 Importe

Mit Import-Klauseln ist es möglich, Packages oder ihre Bestandteile über ihreneinfachen Namen ohne qualifizierende Packages nutzbar zu machen. Beispiels-weise wird nach der Zeile

import java.io.File

mit dem einfachen Bezeichner File auf eben dieses java.io.File verwiesen.Solche Importe können in Scala an beliebigen Stellen stehen. Befindet sich derImport beispielsweise innerhalb einer Funktion wie z.B.

def fun = {import java.io.File// do something with File

}

Page 94: Scala : objektfunktionale Programmierung

4.2 Codeorganisation 81

ist die einfache Bezeichnung File bis zur schließenden geschweiften Klammermöglich. Anschließend muss wieder java.io.File geschrieben werden, außeres gibt auch dort eine Import-Klausel.Es können durch Verwendung des Unterstrichs auch alle Member importiert wer-den. Beispielsweise werden mit

import java.io._

alle Member des Packages java.io importiert. Es ist auch möglich, mit derselbenSyntax die Member eines Objektes zu importieren. Listing 4.36 zeigt ein Beispiel,in dem nach dem Import15 auf das Feld noOfM des Companion-Objektes mit demeinfachen Bezeichner noOfM zugegriffen werden kann. Ohne den Import müsstenwir Mountain.noOfM schreiben. Dies funktioniert selbstverständlich für beliebi-ge Objekte16.

Listing 4.36: Die Klasse Mountain mit Companion-Objekt

class Mountain(val name: String,val height: Int) {

import Mountain._noOfM += 1

}object Mountain {private var noOfM = 0def numberOfMountains = noOfM

}

Statt nur ein Member oder gleich alle zu importieren, können einzelne Member ineiner import selector clause zusammengefasst werden. Beispielsweise werden mit

import java.util.{Calendar, GregorianCalendar}

sowohl java.util.Calendar als auch java.util.GregorianCalendarimportiert. Der Import von java.util.{_} entspricht im Übrigen dem Importvon java.util._. Innerhalb einer Import-Selector-Klausel können einzelne Ele-mente auch umbenannt werden. Beispielsweise ist nach dem Import

import java.util.{Calendar => Cal, GregorianCalendar}

java.util.Calendar unter dem Bezeichner Cal und java.util.Grego-rianCalendar unter dem Namen GregorianCalendar verfügbar. Sollen ein-zelne Elemente umbenannt werden, aber dennoch alle importiert werden, könnenwir dies auch in einem Einzeiler angeben. Z.B. steht im folgenden Beispiel der Un-terstrich für den „Rest”:

import java.util.{Calendar => Cal, _}

15 Diese Art Import entspricht dem static import in Java, mit dem statische Member einer Klasse im-portiert werden können.16 . . . aber aufgrund des private-Modifiers wieder nicht in der Scala-Shell.

Page 95: Scala : objektfunktionale Programmierung

82 4 Reine Objektorientierung

Es ist auch möglich, alles außer bestimmten Elementen zu importieren. Dazu wirdauch die Syntax zur Umbenennung genutzt, allerdings dieses Mal mit dem Un-terstrich als neuem Namen. Mit der folgenden Klausel wird alles aus java.ioaußer der Klasse File importiert:

import java.io.{File => _, _}

Jede Scala-Quellcode-Datei verfügt bereits über drei implizite Importe, nämlich:

import java.lang._import scala._import Predef._

Die Besonderheit im Gegensatz zu normalen Importen ist hier, dass späte-re Importe frühere überschatten. Beispielsweise ist eine Klasse mit dem Na-men StringBuilder sowohl in java.lang als auch in scala enthalten.Durch die obige Reihenfolge referenziert der Bezeichner StringBuilder dannscala.StringBuilder.

4.3 Traits

Scala unterstützt nur Einfachvererbung wie viele moderne objektorientierte Pro-grammiersprachen. Das heißt, eine Klasse kann nur genau eine Basisklasse haben.Natürlich kann die Basisklasse auch schon eine andere Klasse erweitern. Mehr-fachvererbung, wie sie beispielsweise in C++ möglich ist, kann zu einem undurch-sichtigen und komplizierten Design führen und ermöglicht das Auftreten des so-genannten Diamond-Problems17.Das Diamond-Problem entsteht, wenn eine Klasse D von zwei Basisklassen B undC abgeleitet ist und diese beiden wiederum von der Klasse A. Den Namen ver-dankt das Problem der grafischen Darstellung der Beziehung zwischen den vierKlassen, die wie eine Raute (engl. diamond) aussieht. Hat nun A eine Methode, diesowohl in B als auch in C redefiniert wird, ist nicht klar, welche Implementierungfür D gelten sollte.Um nicht ganz auf eine einzige Basisklasse eingeschränkt zu sein, haben viele ob-jektorientierte Programmiersprachen das Konzept der Interfaces eingeführt. EinInterface kann als sehr spezielle abstrakte Klasse gesehen werden, die keine einzi-ge konkrete Implementierung enthält. Das heißt aber, dass jede konkrete Klasse,die ein Interface implementiert, für alle Methoden des Interfaces eine Implemen-tierung enthalten muss.Der Ansatz führt entweder zu sehr schlanken Interfaces, also Interfaces mit sehrwenigen Methoden, deren Implementierungen oft umständlich zu nutzen sind,oder zu Interfaces, bei deren Implementierung eine große Anzahl Methoden zuerstellen sind, die unter Umständen in vielen Klassen gleich aussehen.

17 siehe [TJJV04]

Page 96: Scala : objektfunktionale Programmierung

4.3 Traits 83

Scala hat keine Interfaces, sondern sogenannte Traits, die neben abstrakten Me-thoden auch Implementierungen von Methoden sowie Felder enthalten dürfen.In Abschnitt 4.3.1 werden Traits als Rich Interfaces besprochen. Außerdem ist esmöglich, mithilfe von Traits eine Methode zu verändern. Dies wird in Abschnitt4.3.2 beschrieben.

4.3.1 Rich Interfaces

Zunächst sind die einzigen Unterschiede zwischen abstrakten Klassen und Traits,dass Letztere keine Konstruktoren haben, und das verwendete Schlüsselwort. Dieabstrakte Klasse Animal aus Listing 4.32 auf Seite 76 könnte als Trait wie in Lis-ting 4.37 dargestellt aussehen.

Listing 4.37: Der Trait Animal

import java.util.Calendartrait Animal {var name: Stringval dateOfBirth: Calendardef makeNoise(): Unitdef age = {val today = Calendar.getInstanceval age = today.get(Calendar.YEAR) -

dateOfBirth.get(Calendar.YEAR)today.set(Calendar.YEAR,

dateOfBirth.get(Calendar.YEAR))if (today before dateOfBirth)age-1

elseage

}override def toString = {name+" "+age+" year"+(if (age!=1) "s" else "")+" old"

}}

Die Unterschiede zwischen dem Trait Animal (Listing 4.37) und der abstraktenKlasse Animal (Listing 4.32) sind also

Verwendung des Schlüsselwortes trait statt abstract class und

Definition des Klassen-Parameters als normales Member.

Bei der Definition der Klasse Dog wird im Gegensatz zu Dog aus Listing 4.33auf Seite 77 der Klassen-Parameter dateOfBirth als val definiert und nicht alsKlassen-Parameter an Animal übergeben. Die Klasse Dog, die den Trait Animalverwendet, ist in Listing 4.38 angegeben.

Page 97: Scala : objektfunktionale Programmierung

84 4 Reine Objektorientierung

Listing 4.38: Die Klasse Dog

import java.util.Calendarclass Dog(

var name: String,val dateOfBirth: Calendar = Calendar.getInstance) extends Animal {

def makeNoise() {println("woof, woof")

}}

Auch wenn Dog jetzt den Trait Animal nutzt und nicht von der Klasse erbt, wirddas Schlüsselwort extendsweiterhin verwendet. Ein Trait ist eine spezielle Formdes sogenannten Mixins. Ein Mixin fasst mehrfach verwendbare, zusammengehö-rige Funktionalitäten zu einer Einheit zusammen, die in eine Klasse hineingemixtwerden kann. Der Name kommt vom englischen mix in (einmischen, unterrüh-ren, hineinmixen). Die Besonderheit des Traits ist, dass eine Klasse, die einen Traithineinmixt, aber keine Basisklasse mit extends angibt, die Basisklasse des Traitsübernimmt. In unserem Beispiel hat der Trait Animal die Basisklasse AnyRef, diedamit auch zur Basisklasse von Dog wird.Nachdem der vorgestellte Ansatz, Animal als Trait und nicht als Klasse zu im-plementieren, etwas konstruiert ist, wollen wir das Beispiel nun etwas andersaufbauen. Zunächst definieren wir eine abstrakte Klasse Animal für unsere Tiere(siehe Listing 4.39).

Listing 4.39: Die abstrakte Klasse Animal ohne dateOfBirth und age

abstract class Animal {var name: Stringdef makeNoise(): Unit

}

Allerdings definieren wir die Klasse Animal jetzt ohne dateOfBirth und age,da dies eine Funktionalität ist, die sicher nicht nur bei Tieren benötigt werdenkann. Stattdessen definieren wir einen Trait HasAge (siehe Listing 4.40).

Listing 4.40: Der Trait HasAge

trait HasAge {import java.util.Calendarval dateOfBirth: Calendardef age = {val today = Calendar.getInstanceval age = today.get(Calendar.YEAR) -

dateOfBirth.get(Calendar.YEAR)today.set(Calendar.YEAR,

dateOfBirth.get(Calendar.YEAR))if (today before dateOfBirth)

Page 98: Scala : objektfunktionale Programmierung

4.3 Traits 85

age-1elseage

}}

Wollen wir jetzt die Klasse Dog so implementieren, dass sie die gleiche Funktio-nalität wie zuvor hat, müssen wir die Klasse Animal erweitern und den TraitHasAge hineinmixen. Wird ein Trait zusätzlich zu einer Basisklasse oder zu ei-nem anderen Trait hineingemixt, folgt dieser auf das Schlüsselwort with. WeitereTraits schließen sich dann mit jeweils eigenem with an.

Listing 4.41: Die Klasse Dog mit der Basisklasse Animal und dem Trait HasAge

import java.util.Calendarclass Dog(

var name: String,val dateOfBirth: Calendar = Calendar.getInstance) extends Animal with HasAge {

def makeNoise() {println("woof, woof")

}override def toString = {name+" "+age+" year"+(if (age!=1) "s" else "")+" old"

}}

Die Klasse Dog ist in Listing 4.41 dargestellt. Die wesentliche Änderung ist dieAngabe von with HasAge. Allerdings muss nach diesem Design die toString-Methode in Dog, und analog auch in Cat und Cow, redefiniert werden, da inder Klasse Animal die Methode age nicht bekannt ist und erst durch das TraitHasAge eingebracht wird.Der Ausweg ist aber ganz einfach. Nicht in die Klasse Dog, sondern schon in dieKlasse Animal sollte der Trait HasAge hineingemixt werden. Die Klasse Animalmit dem Trait HasAge ist in Listing 4.42 aufgeführt. Die Klasse Dog kann dannwieder in der Fassung aus Listing 4.38 auf Seite 84 verwendet werden.

Listing 4.42: Die abstrakte Klasse Animal mit dem Trait HasAge

abstract class Animal extends HasAge {var name: Stringdef makeNoise(): Unitoverride def toString = {name+" "+age+" year"+(if (age!=1) "s" else "")+" old"

}}

Es ist darüber hinaus auch möglich, eine anonyme Klasse, die einen Trait nutzt, zudefinieren. Nachdem ein Trait aber keine Klassen- bzw. „Trait”-Variablen zulässt,

Page 99: Scala : objektfunktionale Programmierung

86 4 Reine Objektorientierung

müssen abstrakte Member des Traits entsprechend definiert werden. Betrachtenwir den in Listing 4.43 definierten Trait PositiveNumber.

Listing 4.43: Der Trait PositiveNumber

trait PositiveNumber {val value: Intrequire(value>0)

}

Im Trait PositiveNumber wird ein abstrakter val vom Typ Int definiert. Au-ßerdem wird durch das require sichergestellt, dass der Wert von value nichtkleiner oder gleich Null ist. Wenn wir nun eine anonyme Klasse, die den Traitenthält, definieren wollen, wäre der naheliegende Ansatz, den val value wie inder folgenden Sitzung dargestellt zu definieren:

scala> val n = new PositiveNumber {| val value = 12| }

java.lang.IllegalArgumentException: requirement failedat scala.Predef$.require(Predef.scala:134)...

Wie zu sehen ist, führt dies aber zu einer Exception. Dies liegt daran, dass beimErzeugen eines Objektes zuerst der Standardwert für Int zugewiesen und mitrequire überprüft wird. Erst anschließend würde der Wert auf 12 gesetzt wer-den. Indem wir die Zeile require(value>0) entfernen, können wir das einfachüberprüfen:

scala> trait PositiveNumber {| val value: Int| }

defined trait PositiveNumber

scala> val n = new PositiveNumber {| val value = 12| }

n: java.lang.Object with PositiveNumber =$anon$1@40914272

scala> n.valueres0: Int = 12

Wir wollen aber das require im Trait behalten und müssen daher nach einemanderen Ausweg suchen. Davon gibt es grundsätzlich sogar zwei: vorinitialisier-te Felder und lazy vals. Lazy vals werden in Abschnitt 5.1 besprochen. Ein Feldwird vorinitialisiert, wenn es vor dem Trait in geschweiften Klammern definiertwird. Das heißt, wenn wir unsere positive Zahl n wie im folgenden Listing ange-geben definieren, sieht das require den korrekten Wert 12:

Page 100: Scala : objektfunktionale Programmierung

4.3 Traits 87

scala> val n = new {| val value = 12| } with PositiveNumber

Ein sehr nützlicher, in der Scala-Distribution enthaltener Trait ist Ordered. DerTrait Ordered ist in Listing 4.44 dargestellt.

Listing 4.44: Der Ordered-Trait

trait Ordered[A] extends java.lang.Comparable[A] {def compare(that: A): Intdef < (that: A): Boolean = (this compare that) < 0def > (that: A): Boolean = (this compare that) > 0def <= (that: A): Boolean = (this compare that) <= 0def >= (that: A): Boolean = (this compare that) >= 0def compareTo(that: A): Int = compare(that)

}

Objekte einer Klasse, die den Trait implementiert, können mit <, >, <= und >= ver-glichen werden. Dazu muss nur die abstrakte Methode compare implementiertwerden, die das Objekt (this) mit einem anderen Objekt (that) vergleicht undals Ergebnis ein x liefert, für das gilt:

x < 0 genau dann, wenn this < thatx == 0 genau dann, wenn this == thatx > 0 genau dann, wenn this > that

Wollen wir jetzt beispielsweise dem Trait HasAge aus Listing 4.40 das Featurehinzufügen, alle dem Alter nach vergleichen zu können, müssen wir den Traitwie in Listing 4.45 angegeben erweitern.

Listing 4.45: Der Trait HasAge implementiert den Trait Ordered.

trait HasAge extends Ordered[HasAge] {import java.util.Calendarval dateOfBirth: Calendardef age = {val today = Calendar.getInstanceval age = today.get(Calendar.YEAR) -

dateOfBirth.get(Calendar.YEAR)today.set(Calendar.YEAR,

dateOfBirth.get(Calendar.YEAR))if (today before dateOfBirth)age-1

elseage

}def compare(that: HasAge) = this.age compare that.age

}

Page 101: Scala : objektfunktionale Programmierung

88 4 Reine Objektorientierung

Die einzigen beiden Stellen, die gegenüber dem ursprünglichen Trait aus Listing4.40 anders sind, sind in der ersten Zeile das extends Ordered[HasAge] unddie einzeilige Definition von compare, die sich einfach auf das in der Klasse Intdefinierte compare abstützt.

4.3.2 Stapelbare Modifikationen

Mit Traits ist es möglich, verschiedene Modifikationen hintereinander zu schach-teln. Wir wollen dies mit folgendem einfachen Anwendungsfall zur Eingabe vonZeichenketten verdeutlichen:

1. Eine Zeichenkette soll als Ergebnis einer Funktion get zurückgegeben wer-den.

2. Eine Zeichenkette soll aus verschiedenen Quellen kommen können, z.B. voneiner Benutzereingabe oder aus einer Datei.

3. Bei der Benutzereingabe soll es möglich sein, optional einen Prompt anzuge-ben.

4. Es soll die Möglichkeit geben, die Zeichenkette beim Einlesen, z.B. zur Fehler-suche im Programm, auf der Konsole auszugeben.

5. Es soll die Möglichkeit geben, die Zeichenketten zusätzlich in einer Liste zuspeichern und damit zu einem beliebigen Zeitpunkt auf alle bisher eingelese-nen Zeichenketten erneut zuzugreifen.

6. Es soll die Möglichkeit geben, eine Blacklist mit Zeichenketten zu verwalten.Ist die eingelesene Zeichenkette in der Blacklist, soll sie durch sieben Sternchen(*******) ersetzt werden.

7. Die Punkte 4, 5 und 6 sollen beliebig kombinierbar sein, z.B. mit Blacklistingund Ausgabe auf Konsole.

Die ersten beiden Punkte können durch eine abstrakte Basisklasse oder einen Traitund konkrete Implementierungen gelöst werden. In Listing 4.46 sind die abstrakteBasisklasse Input und die beiden davon abgeleiteten Klassen ReadLineInputund FileInput dargestellt.

Listing 4.46: Die abstrakte Klasse Input und die davon abgeleiteten Klassen ReadLine-Input und FileInput

abstract class Input {def get(): String

}class ReadLineInput extends Input {def get() = readLine()

}class FileInput(filename: String) extends Input {import scala.io.Source

Page 102: Scala : objektfunktionale Programmierung

4.3 Traits 89

import java.io.Fileprivate[this] val contents =Source fromFile (new File(filename)) getLines

def get() = {if (contents.hasNext) contents.next() else ""

}}

Die abstrakte Klasse Input enthält eine abstrakte Methode get. Die abgeleite-te Klasse ReadLineInput implementiert get durch readline. Die abgeleiteteKlasse FileInput definiert einen Klassen-Parameter für den Dateinamen. DerInhalt der Datei wird dann beim Initialisieren eines FileInput-Objektes mithilfedes Source-Companion-Objektes ausgelesen. Die Methode fromFile benötigtals Argument ein java.io.File-Objekt und gibt ein Source-Objekt zurück.Mit der Methode getLines wird schließlich ein Iterator erzeugt, der bei jedemZugriff mit next eine Zeile aus der Datei liefert. Dies wird dann in der Metho-de get genutzt. Sind alle Zeilen der Datei „verbraucht”, gibt hasNext den Wertfalse zurück. In dem Fall ist das Ergebnis von get die leere Zeichenkette.Der dritte Punkt, der optionale Prompt, wird auch sinnvollerweise durch ei-ne weitere abgeleitete Klasse realisiert, in dem Fall abgeleitet von der KlasseReadLineInput. Die Klasse ReadLineInputPrompt ist in Listing 4.47 wieder-gegeben.

Listing 4.47: Die Klasse ReadLineInputPrompt

class ReadLineInputPrompt(prompt: String)extends ReadLineInput {

override def get() = {print(prompt+" ")super.get()

}}

Die Klasse ReadLineInputPrompt hat das Prompt als Klassen-Parameter. DieMethode get wird von ReadLineInput geerbt und so redefiniert, dass zuerstder Prompt und ein Leerzeichen ausgegeben und anschließend die Methode getder Basisklasse mit dem Ausdruck super.get() aufgerufen wird.Die verbliebenen Punkte 4 bis 7 aus der Aufzählung auf Seite 88 lassen sich mitdem Ansatz, speziellere Klassen von den bisherigen abzuleiten, natürlich realisie-ren, aber die Anzahl der Klassen wird sehr groß, nachdem alle denkbaren Kom-binationen realisiert werden sollen.Scala bietet mithilfe von Traits einen sehr eleganten Ausweg. Mit Traits könnennämlich auch Methoden von Klassen modifiziert werden. Diese Modifikationenkönnen dann hintereinander geschaltet werden. Daher sprechen wir von stackablemodifications (stapelbare Modifikationen).

Page 103: Scala : objektfunktionale Programmierung

90 4 Reine Objektorientierung

Um Punkt 4 zu implementieren, definieren wir einen Trait Echo, der die get-Methode so verändert, dass er zusätzlich die eingegebene Zeichenkette ausgibt.Der Trait Echo ist in Listing 4.48 dargestellt.

Listing 4.48: Der Echo-Trait

trait Echo extends Input {abstract override def get() = {val input = super.get()println(input)input

}}

Der Trait Echo hat zwei Besonderheiten. Erstens erweitert der Trait eine Klas-se, nämlich die abstrakte Klasse Input, und zweitens ruft die Methode get mitsuper.get() die abstrakte Methode get aus Input auf. Dies scheint zunächstwenig Sinn zu machen, da der super-Aufruf dann in der Klasse Input fehlschla-gen würde.Durch die Kennzeichnung der Methode als abstract override bekommt dasKonstrukt nun aber folgende Bedeutung: Das extends Input besagt, dass derTrait Echo nur in eine Klasse hineingemixt werden kann, wenn diese Klasse zu-vor durch einen anderen Trait oder eine Klasse die Methode get implementiert.Damit macht der Aufruf super.get() auch Sinn, denn damit wird die get-Methode in der Form aufgerufen, wie sie vor dem Hineinmixen des Traits war. Inder Applikation aus Listing 4.49 sind einige Beispiele zur Verdeutlichung enthal-ten.

Listing 4.49: Das StackableTraits1-Objekt

object StackableTraits1 extends Application {

println("ReadLineInput")(new ReadLineInput).get()

println("ReadLineInput mit Echo")(new ReadLineInput with Echo).get()

println("ReadLineInputPrompt")(new ReadLineInputPrompt(">")).get()

println("ReadLineInputPrompt mit Echo")(new ReadLineInputPrompt(">") with Echo).get()

}

In Listing 4.49 ist außerdem zu sehen, dass ein Trait in ein Objekt mit with hin-eingemixt werden kann. Das folgende Listing zeigt die Ausführung des Objektes

Page 104: Scala : objektfunktionale Programmierung

4.3 Traits 91

StackableTraits1 nach der Kompilierung. Die Kommentare← Eingabe und← Echo wurden nachträglich eingefügt, um deutlich zu machen, wo der Nutzeretwas eingegeben hat und wo die durch den Echo-Trait veränderte Methode dieEingabe wieder ausgegeben hat.

$ scala StackableTraits1ReadLineInputHallo ← EingabeReadLineInput mit EchoHallo ← EingabeHallo ← EchoReadLineInputPrompt> Hallo ← EingabeReadLineInputPrompt mit Echo> Hallo ← EingabeHallo ← Echo

Die nächste geforderte Modifikation ist das Speichern und die Möglichkeit desspäteren Abrufens aller Eingaben. Der Trait Collect (siehe Listing 4.50) setztdies um.

Listing 4.50: Der Trait Collect

trait Collect extends Input {import scala.collection.mutable.ListBufferprivate[this] var inputs = new ListBuffer[String]def inputList = inputs.toListabstract override def get() = {val input = super.get()inputs += inputinput

}}

Die Eingaben werden in einem objekt-privaten ListBuffer gespeichert undüber die Methode inputList als unveränderliche Liste zur Verfügung gestellt.Die Klasse ListBuffer muss explizit importiert werden. Die nächste einge-lesene Zeichenkette kann dem ListBuffer mit der Methode += hinzugefügtwerden. Auch im Trait Collect muss die Methode get wieder als abstractoverride gekennzeichnet werden, um damit eine bereits implementierte Me-thode get abändern zu können.Der Trait Collect kann nun beispielsweise für die Definition der Klasse My-Input wie folgt genutzt werden:

class MyInput extends ReadLineInputPrompt(">")with Collect

Die Klasse MyInput erweitert die Klasse ReadLineInput und fügt anschließendden Trait Collect hinzu.

Page 105: Scala : objektfunktionale Programmierung

92 4 Reine Objektorientierung

Listing 4.51: Das StackableTraits2-Objekt

object StackableTraits2 extends Application {val in = new MyInputin.get()in.get()println(in.inputList)

}

Wird nun die Applikation aus Listing 4.51 übersetzt und gestartet, kann das Er-gebnis folgendermaßen aussehen:

$ scala StackableTraits2> Hallo Welt> Hallo LeserList(Hallo Welt, Hallo Leser)

Interessant ist nun aber insbesondere die Hintereinanderschaltung der beidenTraits Echo und Collect. Definieren wir MyInput als

class MyInput extends ReadLineInputPrompt(">")with Collect with Echo

so sieht die erneute Ausführung von StackableTraits2 so aus:

$ scala StackableTraits2> Hallo WeltHallo Welt> Hallo LeserHallo LeserList(Hallo Welt, Hallo Leser)

Die Methode get der Klasse MyInput wird also sowohl durch Echo als auchdurch Collect verändert. Die Eingabe wird auf dem Bildschirm wieder ausge-geben und im ListBuffer gespeichert.Die letzte Variante, die wir implementieren wollen, ist der Blacklist-Trait (sie-he Listing 4.52).

Listing 4.52: Der Blacklist-Trait

trait Blacklist extends Input {val blacklist: List[String]abstract override def get() = {val input = super.get()if (blacklist contains (input trim))"******"

elseinput

}}

Page 106: Scala : objektfunktionale Programmierung

4.3 Traits 93

Der Trait Blacklist deklariert zunächst einen abstrakten val mit dem Bezeich-ner blacklist als Liste von Strings. Die abgeänderte Methode get verhält sichso, dass für eine eingelesene Zeichenkette überprüft wird, ob sie in der Blacklistenthalten ist. Ist dies der Fall, wird die Zeichenkette, wie auf Seite 88 unter Punkt6 vorgegeben, durch sieben Sternchen ersetzt. Mit der Methode trimwerden vor-her alle Leerzeichen am Anfang und Ende der Zeichenkette entfernt.

Listing 4.53: Das StackableTraits3-Objekt

object StackableTraits3 extends Application {

val inBEC = new ReadLineInputPrompt("BEC>")with Blacklist with Echo with Collect {val blacklist = List("Hello World")

}inBEC.get()println(inBEC.inputList)

val inCBE = new ReadLineInputPrompt("CBE>")with Collect with Blacklist with Echo {val blacklist = List("Hello World")

}inCBE.get()println(inCBE.inputList)

}

Definieren wir nun das in Listing 4.53 dargestellte StackableTraits3-Objektund führen es aus, so passiert Folgendes:

$ scala StackableTraits3BEC> Hello World******List(******)CBE> Hello World******List(Hello World)

Bei der ersten Eingabe wurde Hello World sowohl zur Ausgabe als auch in derListe durch Sternchen ersetzt. Bei der zweiten Eingabe aber nur zur Ausgabe. Inder Liste steht die unveränderte Eingabe.An diesem Beispiel ist sehr schön zu sehen, dass die Reihenfolge wichtig ist, inder die Modifikationen angewendet werden. Die get-Methode von inBCE inListing 4.53 wurde zuerst durch den Trait Blacklist, dann durch Echo undzum Schluss durch Collect modifiziert. Das heißt, zuerst wird die Zeichenket-te Hello World durch die Sternchen ersetzt, dann ausgegeben und dann in dieListe gespeichert.Die Reihenfolge bei inCBE ist eine andere. Dort wird Hello World zuerst indie Liste gespeichert, dann durch Sternchen ersetzt und abschließend ausgege-

Page 107: Scala : objektfunktionale Programmierung

94 4 Reine Objektorientierung

ben. Durch die Veränderung der Reihenfolge zu with Echo with Blacklistwith Collect könnte beispielsweise die Zeichenkette erst unverändert ausge-geben und dann durch Sternchen ersetzt in der Liste gespeichert werden.Betrachten wir noch einmal das Objekt inCBE aus Listing 4.53:

val inCBE = new ReadLineInputPrompt("CBE>")with Collect with Blacklist with Echo {val blacklist = List("Hello World")

}

Durch die rechte Seite des Gleichheitszeichens wird ein Objekt einer anonymenKlasse erzeugt, das die Klasse ReadLineInputPrompt erweitert. Die KlasseReadLineInputPrompt wiederum erweitert ReadLineInput und diese dieabstrakte Klasse Input. Außerdem werden die drei Traits Collect, Blacklistund Echo hineingemixt.Nachdem alle drei Traits und die Klassen ReadLineInput und ReadLine-InputPrompt die Methode get definieren, stellt sich die Frage, welche Metho-de durch den Ausdruck inCBE.get() ausgeführt wird. Bei Klassen, die ein-fach voneinander erben, ist die Frage einfach zu beantworten. Interessant ist aber,wie sich die Traits einordnen, zumal ja auch deren get-Methoden allesamt einensuper-Aufruf enthalten.Die Antwort von Scala auf diese Frage ist die Linearisierung. Durch Linearisierungwerden alle Klassen und Traits in eine wohldefinierte, lineare Ordnung gebracht.Ein super-Aufruf mündet dann einfach in die Methode der nächsten Klasse oderdes nächsten Traits in der linearen Kette.Die lineare Ordnung wird von hinten nach vorne berechnet. Die letzten Gliederin der Kette sind die Basisklasse und deren lineare Ordnung. In unserem Beispielist das also die Kette ReadLineInputPrompt→ ReadLineInput→ Input→AnyRef→ Any.Anschließend werden die Traits von links nach rechts abgearbeitet. Alle Klassen,die bereits in der Linearisierung vorhanden sind, werden bei der Übernahme derKette der Traits weggelassen, d.h. durch with Collect wird die Kette erwei-tert zu Collect → ReadLineInputPrompt → ReadLineInput → Input →AnyRef→ Any.Mit den Traits Blacklist und Echo ist das Resultat der Linearisierung Echo→ Blacklist→ Collect→ ReadLineInputPrompt→ ReadLineInput→Input→ AnyRef→ Any.Das heißt, beim Ausdruck inCBE.get() wird die Methode get aus Echo aufge-rufen. Diese ruft als Erstes mit super.get() die Methode aus Blacklist auf.Diese wiederum get aus Collect usw. Damit wird klar, dass der eingegebeneWert Hello World zuerst in der Liste landet und erst nach Rückkehr der Metho-de aus Collect durch Sternchen ersetzt wird.

Page 108: Scala : objektfunktionale Programmierung

4.4 Implicits und Rich-Wrapper 95

Die Linearisierung von

val inBEC = new ReadLineInputPrompt("BEC>")with Blacklist with Echo with Collect {val blacklist = List("Hello World")

}

lautet demnach Collect → Echo → Blacklist → ReadLineInputPrompt→ ReadLineInput→ Input→ AnyRef→ Any. Daher werden die Sternchen indiesem Fall sowohl ausgegeben als auch in der Liste gespeichert.Durch Veränderung der Reihenfolge, in der die Traits hineingemixt werden, ist esso also auch möglich, die unveränderte Eingabe auszugeben, aber als Sternchenzu speichern.

4.4 Implicits und Rich-Wrapper

Scala verwendet eine Reihe von Java-Typen. Beispielsweise hat die Zeichenketteden Typ java.lang.String. In Scala kann ein Teil von einer Zeichenkette mitder Methode drop zurückgeben werden, z.B.

scala> val str = "Hello World"str: java.lang.String = Hello World

scala> str drop 6res0: String = World

Beim Blick in die Java-API-Dokumentation fällt aber auf, dass eine Methodedrop in der Klasse java.lang.String gar nicht definiert ist. Bei einer Su-che in der Scala-API-Dokumentation finden wir die Methode drop in der Klas-se StringOps. Allerdings handelt es sich bei dem Typ von str um keinenStringOps.18

Die Lösung liegt in den sogenannten impliziten Umwandlungen, kurz Implicits. Da-mit kann ein Objekt bei Bedarf automatisch in ein anderes Objekt einer anderenKlasse umgewandelt werden. Durch diesen Ansatz ist es in Scala möglich, beste-hende Klassen mit zusätzlichen Methoden anzureichern. Die „anderen” Klassenwerden daher auch Rich-Wrapper genannt.Im Beispiel muss also das String-Objekt automatisch in ein StringOps-Objektumgewandelt werden, um die Methode drop anwenden zu können. Die Methodedrop gibt als Ergebnis wieder einen String zurück.

18 Vor Scala 2.8 wurde ein String in einen RichString umgewandelt. In der Klasse RichStringwaren dann die Collection-Methoden wie drop und map definiert. Nachteil des Ansatzes war aber,dass das Ergebnis der Methoden in RichString auch ein RichString und kein String war. Zu-sätzlich zu StringOps gibt es in Scala 2.8 die Klasse WrappedString, deren Methoden dann aucheinen WrappedString zurückliefern.

Page 109: Scala : objektfunktionale Programmierung

96 4 Reine Objektorientierung

Die automatische Umwandlung funktioniert mit ganz normalen Funktionen, diefür den Compiler mit implicit markiert werden. Für unser Beispiel ist das dieMethode

implicit def augmentString(x: String): StringOps

aus dem Objekt PreDef. Das heißt also, beim Ausdruck str drop 6 wird dieZeichenkette Hello World durch augmentString in ein StringOps-Objektumgewandelt. Dieses versteht dann die Nachricht drop mit dem Argument 6und gibt als Ergebnis den Wert World vom Typ String zurück.Die Umwandlungsfunktionen werden vor dem Übersetzen durch den Compilerin den Code eingefügt. Also wird nicht str drop 6 übersetzt, sondern

augmentString(str) drop 6

Der Compiler darf implizite Umwandlungen einfügen, um Typfehler zu verhin-dern, die sonst auftreten würden. Es gibt drei Stellen, an denen Implicits genutztwerden:

1. Umwandlung des Empfängers einer Nachricht.

Dies hat im o.a. Beispiel zur Anwendung von augmentString geführt.

2. Umwandlung in den erwarteten Typ.

Hätten wir z.B.

val str2: scala.collection.immutable.WrappedString =str drop 6

geschrieben, würde der String mit der impliziten Methode wrapString ineinen WrappedString umgewandelt.

3. Implizite Parameter.

Diese werden ab Seite 98 beschrieben.

Die genauen Regeln dazu lauten:

Zur Umwandlung stehen nur die Funktionen zur Verfügung, die mit demSchlüsselwort implicit markiert sind.

Die Definition muss als einfacher Bezeichner sichtbar, d.h. im Scope, sein. Sindz.B. in einem Objekt Sample implizite Umwandlungsfunktionen enthalten,werden sie nach der Import-Klausel import Sample._ als einfache Bezeich-ner eingeblendet und anwendbar.

Außerdem sucht der Compiler in den Companion-Objekten des Quell- undZieltyps. Diese Umwandlungen müssen nicht als einfacher Bezeichner sicht-bar sein.

Page 110: Scala : objektfunktionale Programmierung

4.4 Implicits und Rich-Wrapper 97

Die Umwandlung muss eindeutig sein. Gibt es mehrere Funktionen, die denTypfehler berichtigen könnten, wird keine von beiden angewendet, sondernder Typfehler gemeldet.

Es wird nur eine implizite Umwandlung an einer Stelle eingesetzt. Wenn einTyp A vorliegt und B benötigt wird, wird nur nach direkten Umwandlungenvon A nach B gesucht. Gibt es eine Umwandlung von A nach C und eine weiterevon C nach B, werden die beiden nicht automatisch hintereinander angewen-det.

Implizite Umwandlungen können den Code an vielen Stellen vereinfachen. Wirwollen dies mit einem Beispiel verdeutlichen. In Listing 4.54 werden zwei Klassendefiniert, um Bands und ihre Platten zu repräsentieren.

Listing 4.54: Die Klassen Record und Band

class Record(title:String) {override def toString = title

}class Band(name: String) {private var records: List[Record] = List()def addRecord(record: Record) {records ::= record

}override def toString = name +": "+records

}

Mit diesen Klassen lassen sich nun Bands und Platten erzeugen, z.B.

scala> val jl = new Band("The Jesus Lizard")jl: Band = The Jesus Lizard: List()

scala> jl.addRecord(new Record("Goat"))

scala> println(jl)The Jesus Lizard: List(Goat)

Statt jl.addRecord(new Record("Goat")) wäre es allerdings angenehmer,wenn wir nur jl.addRecord("Goat") schreiben müssten. Eine Möglichkeitwäre, die Klasse Band um eine Methode addRecord(title: String) zu er-weitern. Manchmal ist dies aber nicht wünschenswert oder gar nicht möglich,wenn beispielsweise die Klassen Band und Record Teil einer Bibliothek sind.Definieren wir stattdessen eine implizite Umwandlung einer Zeichenkette in einRecord-Objekt, so erzielen wir dasselbe Ergebnis, wie folgende Sitzung zeigt:

scala> implicit def stringToRecord(title: String) =| new Record(title)

stringToRecord: (title: String)Record

scala> jl.addRecord("Pure")

Page 111: Scala : objektfunktionale Programmierung

98 4 Reine Objektorientierung

scala> println(jl)The Jesus Lizard: List(Pure, Goat)

Implicits sind auch ein gutes Mittel, um neue Syntax zu simulieren. Eine Map kannja beispielsweise durch

Map("A" -> "Augsburg", "B" -> "Berlin")

erzeugt werden. Diese Notation ist keine spezielle Syntax. Die Zeichenkette vordem Pfeil wird in eine ArrowAssoc umgewandelt, und deren Methode -> gibtein Tupel zurück, das in die Map eingefügt werden kann.Eine weitere Anwendung für implicit sind die impliziten Parameter, d.h. Para-meter einer Funktion, die beim Aufruf nicht angegeben werden müssen, sonderndurch den Compiler ergänzt werden.Zur Veranschaulichung wollen wir auf das Input-Beispiel aus Abschnitt 4.3.2 zurEingabe unserer Plattensammlung aufbauen. Zur Vereinfachung ist die Platten-sammlung eine Map, bestehend aus dem Namen der Band und der Liste der Plat-tennamen. In Listing 4.55 ist die Klasse RecordLibrary angegeben.

Listing 4.55: Die Klasse RecordLibrary

class RecordLibrary {private var records = Map[String,List[String]]()def addInteractive(input: Input) = {print("Bandname: ")val bandname = input.get()print("Recordtitle: ")val recordtitle = input.get()records += bandname -> (records get bandname match {case None => List(recordtitle)case Some(rs) => (recordtitle :: rs)

})}override def toString = records.toString

}

Die vier letzten Zeilen der Methode addInteractive geben schon einen kleinenVorgeschmack auf das Pattern Matching, das in Abschnitt 5.4 erläutert wird. DerCode in diesem Beispiel macht Folgendes: Mit der Map-Methode get wird derWert zum Schlüssel bandname ermittelt. Ist kein entsprechender Eintrag in derMap, wird None zurückgegeben. In diesem Fall wird das Tupel aus dem Bandna-men und der Liste mit dem Plattennamen erzeugt. Ist bereits ein Eintrag vorhan-den, wird die Plattenliste rs als Some(rs) zurückgegeben. In diesem Fall wirdmit dem Bandnamen und der vorher vorhandenen Liste ergänzt um den eingege-benen Plattennamen ein neues Tupel erzeugt. Der jeweils erzeugte Eintrag landetschließlich mit += in der Plattensammlung.

Page 112: Scala : objektfunktionale Programmierung

4.4 Implicits und Rich-Wrapper 99

Über den Parameter input kann die Eingabe entsprechend dem in Abschnitt 4.3.2vorgestellten Ansatz konfiguriert werden. Beispielsweise kann dann mit folgen-den Zeilen eine Plattensammlung erzeugt und mit einem Prompt auf einer neuenZeile Band- und Plattenname abgefragt werden:

val myRecords = new RecordLibrarymyRecords.addInteractive(new ReadLineInputPrompt("\n>"))

Nachteil dieses Ansatzes ist, dass bei jedem erneuten Aufruf von addInter-active immer wieder das Input-Objekt übergeben werden muss. Eine Verein-fachung bieten die impliziten Parameter. Der Parameter von addInteractivemuss dazu zunächst mit implicit markiert werden:

...def addInteractive(implicit input: Input) = {...

Damit kann die Methode mit dem Input-Parameter wie bisher genutzt werden.Zusätzlich ist es nun aber möglich, eine Variable vom Typ Input mit dem Schlüs-selwort implicit zu versehen. Auch hier gilt wieder, dass die Variable als einfa-cher Bezeichner sichtbar sein muss. Beispielsweise kann also nach Definition von

implicit val myPreferredInput = new ReadLineInput

und gegebenenfalls entsprechendem Import-Statement die Methode addInter-active ohne Parameter aufgerufen werden.Implizite Parameter sind nicht eingeschränkt auf einen einzelnen Parameter, son-dern das Schlüsselwort implicit bezieht sich auf die gesamte Parameterliste.Um implizite und explizite Parameter zusammen zu nutzen, müssen wir zunächstwissen, dass es in Scala möglich ist, Funktionen mit mehr als einer Parameterlistezu definieren. Das zugrunde liegende Konzept Currysierung kommt aus der funk-tionalen Programmierung und wird daher erst in Kapitel 5 erklärt.Wir können addInteractive beispielsweise so verändern, dass wir eine expli-zite und eine implizite Parameterliste haben, wie in Listing 4.56 dargestellt.

Listing 4.56: BandnameInput, RecordtitleInput und RecordLibrary

class BandnameInput(val input: Input)class RecordtitleInput(val input: Input)class RecordLibrary {private var records = Map[String,List[String]]()def addInteractive(overrideEntry: Boolean = false)

(implicit bandIn: BandnameInput,recordIn: RecordtitleInput) = {

print("Bandname: ")val bandname = bandIn.input.get()print("Recordtitle: ")val recordtitle = recordIn.input.get()records += bandname -> (records get bandname match {

Page 113: Scala : objektfunktionale Programmierung

100 4 Reine Objektorientierung

case None => List(recordtitle)case Some(rs) =>if (overrideEntry) List(recordtitle)else (recordtitle :: rs)

})}override def toString = records.toString

}

Die explizite Parameterliste enthält als einzigen Parameter ein Flag, das aussagt,ob der alte Eintrag für die Band, falls vorhanden, durch die neue Eingabe über-schrieben werden soll oder nicht. Nachdem ein Default-Wert dafür angegebenist, kann der Parameter auch weggelassen werden. Er kann aber nicht implizitübergeben werden, sondern ist durch die Definition bestimmt. Das heißt, der Pro-grammierer der Methode gibt vor, was passiert, wenn kein Argument für denParameter vorhanden ist.Die implizite Parameterliste enthält zwei Einträge: ein Input-Objekt für die Band-eingabe und eines für die Plattentiteleingabe. Nachdem der Compiler die impli-ziten Parameter nur nach dem Typ auswählt, ist es sinnvoll, möglichst selteneTypen zu nutzen. Wären die beiden impliziten Parameter vom Typ Input, sokönnte nicht unterschieden werden. Daher haben wir hier zwei einfache Wrapper-Klassen BandnameInput und RecordtitleInput definiert. Damit definiertder Nutzer der RecordLibrary-Klasse, was passiert, wenn kein Argument fürdie implizite Parameterliste übergeben wird.Definieren wir uns nun zwei passende Werte, wie zum Beispiel

implicit val bandInput = new BandnameInput(newReadLineInput)

implicit val recordInput = new RecordtitleInput(newReadLineInputPrompt(">"))

können wir die Methode durch den Ausdruck myRecords.addInteractive()nutzen.Damit haben Sie nun alles kennengelernt, was eher dem Bereich objektorientierteProgrammierung zuzuordnen ist. Im nächsten Kapitel fahren wir mit den Elemen-ten der funktionalen Programmierung fort.

Page 114: Scala : objektfunktionale Programmierung

Kapitel 5

Funktionales Programmieren

Scala verbindet als Hybridsprache die objektorientierte Programmierung mit derfunktionalen Programmierung1. Unter objektorientierter Programmierung kön-nen sich die meisten etwas Konkretes vorstellen. Was aber ist eine funktionaleProgrammiersprache? Ob eine Programmiersprache das Label „funktional” füh-ren darf, wurde und wird viel diskutiert. In jüngster Zeit entbrannte auch eineDiskussion, ob Scala sich so nennen darf. Martin Odersky, der Schöpfer von Scala,nennt Scala stattdessen in dem Blog-Beitrag [Ode10a] aus dem Januar 2010 einepostfunktionale Sprache.Allein schon für die Frage, was eine funktionale Programmiersprache überhauptist, gibt es verschiedene Antworten. Einigkeit herrscht über den Kern der funk-tionalen Programmierung: die Berechnung durch Auswertung mathematischerFunktionen und die Vermeidung von Zustand und veränderbaren Daten. Dasheißt, wenn wir in Scala val statt var nutzen, sind wir schon ein bisschen funk-tionaler.Mit funktionaler Programmierung gehen eine Vielzahl von Konzepten einher, de-ren Umsetzung in Scala Gegenstand dieses Kapitels sind. Im ersten Abschnitt stel-len wir Ihnen die Bedarfsauswertung vor, mit der die Berechnung eines vals erstdann durchgeführt wird, wenn das erste Mal auf ihn zugegriffen wird. In Ab-schnitt 5.2 werden Funktionen und Rekursionen besprochen. Ein wesentlichesMerkmal funktionaler Programmierung sind die Funktionen höherer Ordnung(siehe Abschnitt 5.3) , die eine Funktion als Argument oder Ergebnis haben kön-nen.Viele funktionale Programmiersprachen unterstützen Pattern Matching, eine Ver-allgemeinerung des switch-case-Konstrukts. Pattern Matching und die soge-nannten Case-Klassen führen wir in Abschnitt 5.4 ein. Das auf den amerika-nischen Mathematiker und Logiker Haskell B. Curry zurückgehende Konzept

1 siehe auch [Bir98], [Hug89], [PH06], [OSG08] und [Oka99]

Page 115: Scala : objektfunktionale Programmierung

102 5 Funktionales Programmieren

der Currysierung ermöglicht es, in Scala eigene Kontrollstrukturen zu definie-ren (siehe Abschnitt 5.5). Anschließend werden wir in Abschnitt 5.6 noch einmalzum Schlüsselwort for zurückkommen, um Ihnen zu zeigen, dass mehr als ei-ne Schleife dahintersteckt. Zu guter Letzt werfen wir in Abschnitt 5.7 noch einenBlick auf das sehr ausgefeilte Typsystem von Scala.

5.1 Lazy Evaluation

Standardmäßig werden in Scala alle Felder einer Klasse beim Erzeugen eines Ob-jektes berechnet. Wir sprechen dabei von der sogenannten eager evaluation. In ei-nigen Fällen ist es aber nicht unbedingt sinnvoll, aufwendige Berechnungen, de-ren Ergebnis vielleicht sogar während der gesamten Existenz eines Objektes nichtbenötigt werden, sofort durchzuführen. Der Schlüssel dazu ist die lazy evaluation,die auch mit Bedarfsauswertung übersetzt werden kann. Damit wird der Wert einesFeldes genau dann berechnet, wenn zum ersten Mal darauf zugegriffen wird. ImGegensatz zu einer Methode wird dieser Wert dann aber in ausgewerteter Formgespeichert. Das heißt, bei einem erneuten Zugriff auf das Feld muss nicht erneutberechnet werden.Scala legt dafür den Modifier lazy fest, der sinnvollerweise nur für vals zulässigist. Solche lazy vals sind überall erlaubt und nicht nur als Felder von Objekten.Mit der folgenden Sitzung wollen wir das Verhalten von lazy vals demonstrieren:

scala> val x = {| print("Geben Sie eine Zahl ein: ")| readInt| }

Geben Sie eine Zahl ein: 12x: Int = 12

scala> lazy val y = {| print("Geben Sie eine Zahl ein: ")| readInt| }

y: Int = <lazy>

scala> print(y)Geben Sie eine Zahl ein: 1313

scala> print(x)12

scala> print(y)13

Page 116: Scala : objektfunktionale Programmierung

5.1 Lazy Evaluation 103

In Listing 4.43 auf Seite 86 haben wir den Trait PositiveNumber definiert. UmIhnen das Zurückblättern zu ersparen, wollen wir ihn hier noch einmal angeben:

trait PositiveNumber {val value: Intrequire(value>0)

}

Beim Erzeugen einer anonymen Klasse stellten wir fest, dass ein val in einer Sub-klasse erst nach dem Aufruf des Konstruktors der Basisklasse initialisiert wird.Gelöst hatten wir das Problem mit den vordefinierten Feldern. Eine zweite Mög-lichkeit, dies zu lösen, sind lazy vals. Allerdings können nur konkrete vals lazysein. Daher ist es nicht möglich, den val value mit dem Modifier lazy zu ver-sehen.

Listing 5.1: Der Trait PositiveNumber mit einer lazy val

trait PositiveNumber {val inValue: Intlazy val value = {require(inValue>0)inValue

}}

Die in Listing 5.1 zugegebenermaßen etwas umständliche Version des Posi-tiveNumber-Traits lässt nun eine Definition der folgenden Form zu:

scala> val n = new PositiveNumber {| val inValue = 12| }

n: java.lang.Object with PositiveNumber =$anon$1@409a44d6

Zu beachten ist hier allerdings, dass ein Objekt mit dem Wert 0 für den inValuenun erfolgreich erzeugt werden kann. Die durch require ausgelöste Exceptionwird dann erst beim ersten Zugriff auf value geworfen. Die folgende Beispielsit-zung soll dies verdeutlichen:

scala> val m = new PositiveNumber {| val inValue = 0| }

m: java.lang.Object with PositiveNumber =$anon$1@2754de0b

scala> m.valuejava.lang.IllegalArgumentException: requirement failed

at scala.Predef$.require(Predef.scala:134)...

Page 117: Scala : objektfunktionale Programmierung

104 5 Funktionales Programmieren

Damit ein lazy val x Sinn macht, müssen auch alle anderen vals, die in ihrerBerechnung auf x zugreifen, lazy sein. Ansonsten würde der Wert von x sofortbenötigt und damit berechnet. Nachdem vars nicht lazy sein können, sollten die-se den Wert von x nicht für ihre Berechnungen benötigen oder x sollte nicht lazysein.

5.2 Funktionen und Rekursionen

Ein wesentliches Merkmal funktionaler Programmierung ist die Unterstützungvon Funktionen als first class values. Das heißt, Funktionen sind Werte und kön-nen als solche genau wie Zahlen oder Zeichenketten zum Beispiel in Listen ge-speichert werden oder sogar Parameter oder Ergebnisse von anderen Funktionensein.Funktionen können nicht nur wie bisher besprochen als Funktionsgleichung, son-dern auch als Funktionsliteral definiert werden. Beispielsweise kann die Funktion

def inc(i: Int) = i + 1

auch als Funktionsliteral

(i: Int) => i + 1

geschrieben werden.2 Nachdem durch (i: Int) => i + 1 ein Wert definiertwird, kann dieser einer Variablen zugewiesen werden. Wir können also schreiben

val inc = (i: Int) => i + 1

Weil sich damit hinter inc eine Funktion verbirgt, können wir inc auf eine Zahlanwenden:

scala> val inc = (i: Int) => i + 1inc: (Int) => Int = <function1>

scala> inc(2)res0: Int = 3

Wir sehen auch in obiger Sitzung den Typ der Funktion (Int) => Int, alsoeine Funktion mit einer Parameterliste, die einen Int enthält, und dem Ergebni-styp Int. Für Funktionen mit 0 bis 22 Parametern sind in der Scala-DistributionTraits definiert. Die Ausgabe <function1> hinter dem Typ ist das Ergebnis dertoString-Methode des Traits Function1. Das heißt auch insbesondere, dassjede Funktion in Scala ein Objekt ist.Es ist auch möglich, ein Funktionsliteral zu definieren, das freie Variablen hat, z.B.

val adder = (i: Int) => i + a

2 Der Ansatz kommt aus dem Lambda-Kalkül, in dem diese Funktion als λx.x + 1 geschrieben wird.Siehe auch [Pie02].

Page 118: Scala : objektfunktionale Programmierung

5.2 Funktionen und Rekursionen 105

Die Variable i ist ein formaler Parameter, aber die Variable a ist eine Referenz aufeine Variable im umschließenden Gültigkeitsbereich. Daher muss eine Variablemit dem Bezeichner a bei der Definition von adder sichtbar sein. Der Compi-ler erzeugt für adder dann eine sogenannte Closure, die die Variable a captured.Nachdem sich in Scala die Werte von Variablen ändern können, wird nicht deraktuelle Wert von a, sondern eine Referenz auf die Variable in die Closure ein-gebunden. Damit wirken sich Veränderungen von a nach Definition von adderunmittelbar aus, wie die folgende Sitzung veranschaulichen soll:

scala> var a = 10a: Int = 10

scala> val adder = (i: Int) => i + aadder: (Int) => Int = <function1>

scala> adder(2)res0: Int = 12

scala> a = 20a: Int = 20

scala> adder(2)res1: Int = 22

Die Variable a hat zunächst den Wert 10. Folglich wird die Anwendung der Funk-tion adder auf den Wert 2 zu 12 evaluiert. Wird anschließend a auf 20 gesetzt,so ändert sich auch das Ergebnis von adder(2) entsprechend.Da es nach dem Konzept der funktionalen Programmierung keine veränderlichenVariablen gibt, also nur vals, ist es, wenn wir rein funktional programmieren,nicht möglich, durch Schleifen Werte zu berechnen. Beispielsweise ist der Code inListing 5.2 zur Berechnung der Summe der in der Liste list enthaltenen Zahlenimperativ, da er die Berechnung durch Änderung des Zustands, hier dem Wertder Variablen sum, durchführt.

Listing 5.2: Imperative Berechnung der Summe der Zahlen einer Liste

def sum(list: List[Int]) = {var sum = 0for (elem <- list) sum += elemsum

}

In der funktionalen Programmierung wird eine solche Programmieraufgabedurch Rekursion gelöst.3 Eine funktionale Version von sum ist in Listing 5.3 dar-gestellt. Nachdem die Funktion einen rekursiven Aufruf enthält, kann der Ergeb-nistyp nicht ermittelt werden und muss daher angegeben werden.

3 Oder besser noch durch Funktionen höherer Ordnung, die wir Ihnen in Abschnitt 5.3 vorstellenwerden.

Page 119: Scala : objektfunktionale Programmierung

106 5 Funktionales Programmieren

Listing 5.3: Rekursive Berechnung der Summe der Zahlen einer Liste

def sum(list: List[Int]): Int = {if (list.isEmpty) 0else list.head + sum(list.tail)

}

Rekursion bedeutet, dass die Funktion sich selbst wieder aufruft. Die Funktionsum in Listing 5.3 addiert das erste Element zur Summe der Restliste. Die Me-thode isEmtpy gibt true zurück, wenn die Liste leer ist, head gibt das ersteElement und tail die Restliste zurück. Ist die Liste leer, ist das Ergebnis 0. Auf-grund der Kürze und Klarheit und der Vermeidung von Zustandsänderungen istdie rekursive Version von sum vorzuziehen. Aber Rekursion ist leider in der Re-gel weniger effizient als ein imperativer Ansatz. Dies ist auch einer der Gründe,warum sich die Sprachdesigner von Scala dazu entschieden haben, imperativesProgrammieren zuzulassen: Weil es zu effizienteren Lösungen führen kann.Dieser Nachteil des rekursiven Ansatzes lässt sich in einem bestimmten Fall vomCompiler weg optimieren. Und zwar dann, wenn die Funktion endrekursiv (tailrecursive) ist, d.h. wenn das Letzte, was in der Funktion berechnet wird, der re-kursive Aufruf selbst ist. Der Compiler kann in diesem Fall die Rekursion durcheine Schleife ersetzen. Die sum-Funktion in Listing 5.3 lässt sich nicht optimieren,da das Ergebnis des rekursiven Aufrufs zum Schluss noch zum ersten Elementder Liste addiert wird. Listing 5.4 zeigt eine Funktion sum, die eine endrekursiveHilfsfunktion nutzt. Diese kann durch den Compiler optimiert werden. Der Hilfs-funktion wird der bisher berechnete Wert und die Restliste übergeben, sodass dieAddition vor dem rekursiven Aufruf berechnet wird und somit der rekursive Auf-ruf die letzte Berechnung von sumHelper ist.

Listing 5.4: Endrekursive Berechnung der Summe der Zahlen einer Liste

def sum(list: List[Int]) = {def sumHelper(x: Int, list: List[Int]): Int = {if (list.isEmpty) xelse sumHelper(x+list.head, list.tail)

}sumHelper(0,list)

}

Vor dem Release von Scala 2.8.0 war die gängige Möglichkeit zu testen, ob dieEndrekursion durch eine Schleife optimiert wurde, im Abbruchfall der Rekursioneine Exception zu werfen und dann im Stacktrace zu überprüfen, ob die Funktionsich selbst aufgerufen hat oder nicht.

Page 120: Scala : objektfunktionale Programmierung

5.2 Funktionen und Rekursionen 107

Listing 5.5: Rekursive Berechnung der Summe der Zahlen einer Liste mit einer @tailrec-Annotation

import scala.annotation.tailrec// wird nicht kompiliert, weil nicht endrekursiv@tailrec def sum(list: List[Int]): Int = {if (list.isEmpty) 0else list.head + sum(list.tail)

}

Seit Scala 2.8.0 gibt es dafür eine Annotation: @tailrec. Versehen wir beispiels-weise, wie in Listing 5.5 zu sehen, die nicht endrekursive Funktion sum aus Listing5.3 mit der @tailrec-Annotation, so führt der Übersetzungsversuch zu folgen-dem Fehler

<console>:6: error: could not optimize @tailrec annotatedmethod: it contains a recursive call not in tailposition

@tailrec def sum(list: List[Int]): Int = {^

Andererseits wird die Funktion sum aus Listing 5.4 mit einem @tailrec vor dersumHelper-Funktion ohne Fehlermeldung kompiliert, das heißt also, sumHel-per konnte optimiert werden.Wie die folgende Sitzung zeigt, kann eine endrekursive Funktion nicht immeroptimiert werden:

scala> import scala.annotation.tailrecimport scala.annotation.tailrec

scala> class Tailrec {| @tailrec def g(i: Int):Int = if (i==0) 0| else g(i-1)| }

<console>:7: error: could not optimize @tailrec annotatedmethod: it is neither private nor final so can beoverridden

@tailrec def g(i: Int):Int = if (i==0) 0

Wie aus der Fehlermeldung ersichtlich ist, wird eine Methode, die in einer Sub-klasse redefiniert werden kann, nicht optimiert.Der Kern des Problems rekursiver Funktionen ist, das der Stack mit jedem Funk-tionsaufruf wächst, was letztendlich je nach Speichergröße und Anzahl der Re-kursionschritte zu einem Überlauf und damit einem Absturz des Programmesführen kann. Ein Mittel, dies zu umgehen, sind sogenannte Trampolines4. Dabeiwerden die Funktionen so implementiert, dass sie statt eines Wertes eine Funktionzurückgeben, mit der der nächste Schritt berechnet werden kann. Das heißt, derrekursive Aufruf findet nicht in der Funktion selbst statt, sondern die Trampoline-

4 Die Funktionen prallen wie von einem Trampolin zurück – daher der Name.

Page 121: Scala : objektfunktionale Programmierung

108 5 Funktionales Programmieren

Funktion führt einen Schritt nach dem anderen aus. Mit Scala 2.8.0 werden solcheTrampoline-Funktionen durch das Objekt scala.util.control.TailCallsunmittelbar unterstützt.Interessant ist Trampolining insbesondere bei wechselseitiger Rekursion, alsowenn zwei oder mehr Funktionen sich gegenseitig aufrufen, was nicht automa-tisch optimiert werden kann. Der Klassiker sind die beiden Funktion isEven undisOdd, die berechnen, ob eine Zahl gerade bzw. ungerade ist und die (wie aus Lis-ting 5.6 ersichtlich) wechselseitig rekursiv definiert sind5.

Listing 5.6: Wechselseitig-rekursive Funktionen isEven und isOdd

def isEven(n: Int): Boolean =if (n==0) true else isOdd(n-1)

def isOdd(n: Int): Boolean =if (n==0) false else isEven(n-1)

Die beiden Funktionen können nicht optimiert werden und führten auf einemTestrechner bei isEven(100000) zu einem StackOverflowError. Durch denTrampoline-Ansatz, der in Listing 5.7 dargestellt ist, wird zwar immer noch fürjede Zahl zwischen der eingegebenen und 0 die Funktion isEven oder isOddaufgerufen, aber jetzt hintereinander.

Listing 5.7: Wechselseitig-rekursive Funktionen isEven und isOdd unter Verwendungdes Tramponline-Ansatzes

import scala.util.control.TailCalls._def isEven(n: Int): TailRec[Boolean] =if (n==0) done(true) else tailcall(isOdd(n-1))

def isOdd(n: Int): TailRec[Boolean] =if (n==0) done(false) else tailcall(isEven(n-1))

Statt eines Booleans geben diese beiden Funktionen eine TailRec[Boolean]zurück. Um die eingebettete Rekursion zu beenden, dient die Funktion done, fürden Rekursionsschritt die Funktion tailcall. Der Ausdruck isEven(100000)gibt damit eine Funktion zurück, die mit result ausgewertet werden kann. DerAusdruck isEven(100000).result berechnet ohne Stack Overflow den Werttrue.

5.3 Higher-Order-Functions

Funktionen, die Funktionen als Parameter haben oder als Ergebnis zurückgeben,werden Funktionen höherer Ordnung genannt. Diese sind eine unmittelbare Folgedaraus, dass Funktionen first class values sind, also gleichberechtigt neben anderen

5 Durch die wechselseitige Abhängigkeit lassen sich die beiden Funktionen nicht nacheinander in dieScala-Shell eingeben.

Page 122: Scala : objektfunktionale Programmierung

5.3 Higher-Order-Functions 109

Werten stehen. Funktionen höherer Ordnung sind ein sehr praktisches Mittel zurAbstraktion, wie wir Ihnen im folgenden Beispiel zeigen wollen.Betrachten wir eine Funktion incList, die alle Zahlen in einer Liste inkremen-tiert. Wir könnten diese Funktion rekursiv definieren, beispielsweise wie in Lis-ting 5.8 angegeben.

Listing 5.8: Die Funktion incList zum rekursiven Inkrementieren aller Elemente einerListe

def incList(list: List[Int]): List[Int] = {if (list.isEmpty) List()else list.head+1 :: incList(list.tail)

}

Weiter könnten wir vielleicht auch eine Funktion benötigen, die alle Elemente ei-ner Liste verdoppelt. Die entsprechende Funktion ist in Listing 5.9 dargestellt.

Listing 5.9: Die Funktion doubleList zum rekursiven Verdoppeln aller Elemente einerListe

def doubleList(list: List[Int]): List[Int] = {if (list.isEmpty) List()else list.head*2 :: doubleList(list.tail)

}

Die beiden Funktionen incList und doubleList unterscheiden sich nur sehrwenig. Wo bei incList der Ausdruck list.head+1 steht, heißt es bei double-List stattdessen list.head*2, und natürlich steht beim rekursiven Aufruf ein-mal incList und einmal doubleList.Definieren wir zwei Funktionen:

def inc(x: Int) = x + 1def double(x: Int) = x * 2

und nutzen diese, wie in Listing 5.10 dargestellt, sehen die Funktionen sich nochein bisschen ähnlicher.

Listing 5.10: Die Funktionen incList und doubleList mit Nutzung der Funktioneninc und double

def incList(list: List[Int]): List[Int] = {if (list.isEmpty) List()else inc(list.head) :: incList(list.tail)

}def doubleList(list: List[Int]): List[Int] = {if (list.isEmpty) List()else double(list.head) :: doubleList(list.tail)

}

Page 123: Scala : objektfunktionale Programmierung

110 5 Funktionales Programmieren

Mit einer Funktion höherer Ordnung könnten wir die Funktion inc bzw. doubleals Argument übergeben. Das heißt, statt der beiden nahezu identischen Funktio-nen incList und doubleList können wir die Funktion funList (siehe Listing5.11) definieren, die als Parameter eine Funktion erwartet, mit der die Listenele-mente verändert werden sollen.

Listing 5.11: Die Funktionen funList mit einer Funktion zur Veränderung der einzelnenElemente als Parameter

def funList(fun: Int => Int ,list: List[Int]): List[Int] = {

if (list.isEmpty) List()else fun(list.head) :: funList(fun, list.tail)

}

Mit der Funktion funList sowie inc und double sind wir in der Lage, dieFunktionen incList und doubleList wie folgt zu definieren:

def incList (list: List[Int]) = funList(inc ,list)def doubleList(list: List[Int]) = funList(double,list)

Tatsächlich wird dieser Ansatz, eine Funktion über eine Liste zu mappen, als Me-thode bereits zur Verfügung gestellt. Statt funList selbst zu definieren, könnenwir die Higher-Order-Function map nutzen:

def incList (list: List[Int]) = list map incdef doubleList(list: List[Int]) = list map double

Als nächsten Schritt können wir uns die Definition von inc und double sparen,indem wir stattdessen Funktionsliterale übergeben:

def incList (list: List[Int]) = list map (i => i + 1)def doubleList(list: List[Int]) = list map (i => i * 2)

Und zu guter Letzt können wir sogar noch die gebundene Variable i im Ausdruckrechts vom Pfeil durch einen Platzhalter ersetzen und schreiben:

def incList (list: List[Int]) = list map (_ + 1)def doubleList(list: List[Int]) = list map (_ * 2)

An die Stelle des Unterstrichs setzt der Compiler dann das jeweilige Element.Damit sparen wir uns die Einführung eines Namens für die Variable. Mit demUnterstrich definieren wir eine sogenannte partiell angewandte Funktion (partiallyapplied function). Dies funktioniert auch mit mehreren Variablen und mehrerenUnterstrichen. Beispielsweise definieren wir durch

scala> val add = (_: Int)+(_: Int)add: (Int, Int) => Int = <function2>

eine Funktion add, die eine Parameterliste mit zwei Argumenten erwartet. DerTypinferenzmechanismus kann für die beiden Argumente von add keinen Typ

Page 124: Scala : objektfunktionale Programmierung

5.3 Higher-Order-Functions 111

ermitteln, sodass wir Typinformationen hinzufügen müssen. Gäben wir keinenTyp an, sähe das in der Scala-Shell folgendermaßen aus:

scala> val add = _+_<console>:5: error: missing parameter type for expanded

function ((x$1, x$2) => x$1.$plus(x$2))val add = _+_

^<console>:5: error: missing parameter type for expanded

function ((x$1: <error>, x$2) => x$1.$plus(x$2))val add = _+_

^

Die korrekt definierte Funktion add kann auf eine Argumentliste, bestehend auszwei Ints, angewendet werden:

scala> add(2,3)

Was hier direkt wie ein Funktionsaufruf aussieht, entspricht eigentlich add.-apply(2,3), also das Objekt mit dem Namen add reagiert auf die Nachrichtapply mit einer Argumentliste, die zwei Ints enthält.In der funktionalen Programmierung dominiert die Liste als Datenstruktur. Fürsie sind in der Regel eine Vielzahl von Funktionen höherer Ordnung definiert, umdie Liste in eine neue zu transformieren. Das neue Collection-Framework (sieheAbschnitt 6.2) von Scala 2.8 definiert viele dieser Funktionen für alle Collections.Wir erläutern einige der Funktionen im Folgenden am Beispiel der Liste.Neben dem bereits vorgestellten map, das alle Elemente einer Liste mit der überge-benen Funktion verändert und die neue Liste zurückgibt, gibt es auch die Funkti-on foreach, die eine Funktion vom Typ A => Unit als Argument hat und diesemit allen Elementen ausführt. Beispielsweise können mit

list.foreach(e => println(e))

alle Elemente einer Liste list zeilenweise ausgegeben werden. Auch hier kanndie gebundene Variable e wieder durch den Unterstrich ersetzt werden:

list.foreach(println(_))

Statt println(_) kann auch println _6 geschrieben werden. Damit steht derPlatzhalter nicht mehr für den einen Parameter, sondern für die gesamte Parame-terliste, die in diesem Fall einen Parameter enthält. Wird im Kontext eine Funktionerwartet, kann ein Platzhalter für die gesamte Parameterliste sogar weggelassenwerden, wie beispielsweise bei

list.foreach(println)

6 Es ist wichtig, zwischen println und dem Unterstrich ein Leerzeichen einzufügen. Zusammenge-schrieben, also println_, ist es ein gültiger Bezeichner.

Page 125: Scala : objektfunktionale Programmierung

112 5 Funktionales Programmieren

In Infix-Operatorschreibweise kann dann auch geschrieben werden:

list foreach println

Eine ganze Reihe von Higher-Order-Functions haben Prädikate, also Funktionenmit dem Ergebnistyp Boolean, als Parameter. Damit lässt sich beispielsweise einTeil einer Liste ermitteln, für den ein Prädikat gilt oder nicht. Mit list filter(_<3) wird eine neue Liste berechnet, die alle Elemente enthält, die kleiner als 3sind. Die Funktion filterNot invertiert das Prädikat. dropWhile entfernt dieElemente, solange eine Bedingung gilt, takeWhile nimmt nur diese, span teiltdie Liste gemäß des Prädikats in zwei Teillisten. Für jedes Prädikat p und jedeListe l gilt:

l span p ≡ (l takeWhile p, l dropWhile p)

wie das folgende Listing verdeutlichen soll:

scala> val list = List(1,2,3,4,5)list: List[Int] = List(1, 2, 3, 4, 5)

scala> list span (_%2==1)res0: (List[Int], List[Int]) = (List(1),List(2, 3, 4, 5))

scala> list takeWhile (_%2==1)res1: List[Int] = List(1)

scala> list dropWhile (_%2==1)res2: List[Int] = List(2, 3, 4, 5)

Die Funktion partition teilt die Liste in zwei Listen, bei denen alle Elementeder ersten Liste das Prädikat erfüllen und alle der zweiten Liste nicht, z.B.

scala> list partition (_%2==1)res3: (List[Int], List[Int]) = (List(1, 3, 5),List(2, 4))

Weitere nützliche Funktionen sind forall und exists, die ermitteln, ob ein Prä-dikat für alle bzw. für mindestens ein Element der Liste erfüllt ist, z.B.

scala> list forall (_<3)res4: Boolean = false

scala> list exists (_<3)res5: Boolean = true

Eine typische Anwendung für Listen ist es, die Elemente mithilfe einer Operati-on zu einem Wert zusammenzufassen, z.B. die Addition aller Elemente. DiesesZusammenfassen wird Falten genannt, da die Liste zu einem Wert zusammen-gefaltet wird. Wie viele funktionale Programmiersprachen stellt auch Scala da-für Funktionen bereit, und zwar für das Falten einer Liste von links beginnendfoldLeft und von rechts beginnend foldRight. Üblicherweise wird beim Fal-

Page 126: Scala : objektfunktionale Programmierung

5.3 Higher-Order-Functions 113

ten keine Rücksicht genommen, ob es sich um eine leere oder eine nicht-leere Listehandelt. Daher wird als Parameter zusätzlich zur Operation ein Wert benötigt, derzum einen Ergebnis der Anwendung der Faltungsfunktion auf die leere Liste istund zum anderen am Ende bzw. am Anfang der Faltung mithilfe der Operationmit dem Rest verknüpft wird.Der Ausdruck

list.foldLeft(0)(_+_)

berechnet die Summe der Elemente der Liste list mit dem Startwert 0, indem ersukzessive alle Elemente mit der Operation + verknüpft.In Scala werden statt der Funktionen foldLeft und foldRight die Operatoren/: und :\ verwendet. Die beiden Operatoren entsprechen den beiden Funktio-nen, wobei gemäß der Regel für Operatoren, die mit einem : enden, bei /: dieListe rechts und der Startwert links stehen. Damit sieht die typische Anwendungnatürlicher und leichter verständlich aus, wie beispielsweise bei

(list :\ 0)(_+_)

oder

(0 /: list)(_+_)

Im ersten Fall wird die Zahl 0, die rechts vom Operator steht, von rechts zum Restaddiert und im zweiten Fall entsprechend von links. Mit der folgenden Sitzungwird es noch etwas deutlicher:

scala> ("Anfang -> " /: list)(_+_)res6: java.lang.String = Anfang -> 12345

scala> (list :\ " <- Ende")(_+_)res7: java.lang.String = 12345 <- Ende

Die Verwendung von foldLeft bzw. foldRight führt zwar zum selben Ergeb-nis, der Ausdruck ist aber weniger eingängig:

scala> (list foldLeft "Anfang -> ")(_+_)res8: java.lang.String = Anfang -> 12345

scala> (list foldRight " <- Ende")(_+_)res9: java.lang.String = 12345 <- Ende

Für nicht-leere Listen gibt es noch die Varianten reduceLeft und reduceRightdie keinen Startparameter benötigen. Die Funktionen scanLeft und scanRightberechnen die Liste aller Zwischenergebnisse, wie im folgenden Listing zu sehen:

scala> (list scanLeft "Anfang -> ")(_+_)res10: List[java.lang.String] = List(Anfang -> , Anfang

-> 1, Anfang -> 12, Anfang -> 123, Anfang -> 1234,Anfang -> 12345)

Page 127: Scala : objektfunktionale Programmierung

114 5 Funktionales Programmieren

scala> (list scanRight " <- Ende")(_+_)res11: List[java.lang.String] = List( <- Ende, 5 <- Ende,

45 <- Ende, 345 <- Ende, 2345 <- Ende, 12345 <- Ende)

Auch wenn wir noch nicht alle standardmäßig verfügbaren Funktionen höhererOrdnung angesprochen haben, hören wir nun damit auf und verweisen für wei-tere Informationen auf das Studium des Scala-APIs.Funktionen höherer Ordnung können Funktionen nicht nur als Parameter haben,sondern auch als Ergebnis zurückgeben. Beispielsweise können wir mit

def mkAdder(x: Int) = (y: Int) => x + y

eine Funktion mkAdder definieren, die angewendet auf einen Parameter x eineFunktion zurückgibt, die x zum jeweiligen Parameter addiert, zum Beispiel:

scala> val plus2 = mkAdder(2)plus2: (Int) => Int = <function1>

scala> plus2(5)res12: Int = 7

Um Ihnen zum Abschluss der Abschnitts noch einen Eindruck zu geben, wiemächtig das Programmieren mit Funktionen höherer Ordnung sein kann, betrach-ten Sie den folgenden Ausdruck:

(1 to 10).toList map (x => x+(_: Int)) map (_(3))

Der erste Teil, (1 to 10).toList map (x => x+(_:Int)), erzeugt aus derListe der Zahlen 1 bis 10 Funktionen entsprechend der Funktion mkAdder (sieheoben). Der Teil map (_(3)) wendet dann jede Funktion aus der Liste auf dieZahl 3 an, d.h. das Ergebnis des Ausdrucks lautet List(4, 5, 6, 7, 8, 9,10, 11, 12, 13). Beim Teilausdruck _(3) ist der Platzhalter für die Funktionda. Das Argument, die Zahl 3, steht bereits in Klammern.

5.4 Case-Klassen und Pattern Matching

Das Pattern Matching, der Vergleich mit einem Muster, ist eine Art verallgemei-nertes switch-case, wie es aus den C-ähnlichen Sprachen bekannt ist. Wie mitswitch und case kann eine Variable auf verschiedene Werte überprüft werden,z.B.

number match {case 1 => println("eine Eins")case 2 => println("eine Zwei")case _ => println("etwas anderes")

}

Page 128: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 115

Hier wird der Wert der Variablen number zuerst mit dem Wert 1 verglichen. Wennder Vergleich erfolgreich ist, wird der Code nach dem Doppelpfeil => ausgeführt.Damit wird beim Wert 1 eine Eins ausgegeben. Anschließend wird der gesam-te Block beendet. Ist number 2, passt der Wert nicht auf das Muster 1. Daher wirdals Nächstes geprüft, ob er auf das Muster 2 passt. Nachdem dies der Fall ist, wirdeine Zwei ausgegeben. Passt ein anderer Wert weder auf das Muster 1 noch auf2, wird mit dem dritten Muster, dem Unterstrich, gematcht. Der Unterstrich istdas Wildcard-Pattern, auf das alles passt.Scala überprüft beim Kompilieren, ob es Fälle gibt, die nicht erreicht werden kön-nen. Setzen wir das Wildcard-Pattern an den Anfang, so würde das bedeuten,dass bei jedem beliebigen Wert von number der erste Fall passt und damit immeretwas anderes ausgegeben wird. Allerdings schlägt schon der Versuch fehl,diesen Ausdruck zu übersetzen, wie die folgende Sitzung zeigt:

scala> number match {| case _ => println("etwas anderes")| case 1 => println("eine Eins")| case 2 => println("eine Zwei")| }

<console>:9: error: unreachable codecase 1 => println("eine Eins")

^<console>:10: error: unreachable code

case 2 => println("eine Zwei")^

Wollen wir eine Funktion definieren, die mittels Pattern Matching ihre Argumenteunterscheidet, so können wir das machen wie in Listing 5.12.

Listing 5.12: Definition der Funktion checkNumber mit Pattern Matching

def checkNumber(number: Int) = number match {case 1 => println("eine Eins")case 2 => println("eine Zwei")case _ => println("etwas anderes")

}

Es ist natürlich auch möglich, den Ausdruck als Funktionsliteral anzugeben (sieheListing 5.13).

Listing 5.13: checkNumber mit Pattern Matching als Funktionsliteral

val checkNumber = (number: Int) => number match {case 1 => println("eine Eins")case 2 => println("eine Zwei")case _ => println("etwas anderes")

}

Page 129: Scala : objektfunktionale Programmierung

116 5 Funktionales Programmieren

Scala erlaubt hier eine noch etwas prägnantere Schreibweise, entsprechend denpartiell angewandten Funktionen (siehe Seite 110), bei der wir uns die Benen-nung der Variablen number sparen können. Die kürzere Form ist in Listing 5.14dargestellt.

Listing 5.14: checkNumber mit Pattern Matching als Funktionsliteral ohne explizite Para-meterdarstellung

val checkNumber: Int => Unit = {case 1 => println("eine Eins")case 2 => println("eine Zwei")case _ => println("etwas anderes")

}

Wollen wir im letzten Fall auch die übergebene Zahl ausgeben, können wir auchauf eine Variable matchen. Auf eine Variable kann wie auf den Unterstrich allesgematcht werden. In Listing 5.15 wird im dritten Fall auf die Variable numbergematcht. Diese damit neu eingeführte Variable kann dann auf der rechten Seitedes Doppelpfeils genutzt werden.

Listing 5.15: checkNumber mit Pattern Matching auf eine Variable

val checkNumber: Int => Unit = {case 1 => println("eine Eins")case 2 => println("eine Zwei")case number => println("etwas anderes: "+number)

}

Listing 5.16 zeigt die Möglichkeit, über Pattern Matching den Typ eines Wertes zuermitteln. Die Funktion checkValue hat einen Parameter vom Typ Any. Damitkann checkValue beispielsweise auf einen Int, einen Double, einen Stringoder irgendetwas anderes angewendet werden. Das Pattern i: Int passt dannfür einen beliebigen Wert vom Typ Int.

Listing 5.16: checkValue mit Pattern Matching auf Typen

val checkValue: Any => Unit = {case i: Int => println("ein Int: "+i)case d: Double => println("ein Double: "+d)case _ => println("etwas anderes")

}

Selbstverständlich können wir auch die verschiedenen Muster gemischt in einerFunktion nutzen, wie in Listing 5.17 dargestellt ist.

Page 130: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 117

Listing 5.17: checkValue mit Pattern Matching auf einen Wert und auf Typen

val checkValue: Any => Unit = {case 1 => println("eine Eins")case _: Int => println("irgendein Int (keine Eins)")case d: Double => println("irgendein Double: "+d)case any => println("etwas anderes: "+any)

}

Die bisher verwendete Darstellung von Sequenzen wie z.B. Listen als List(1,2, 3) oder 1 :: 2 :: Nil7 steht auch als Pattern zur Verfügung. Damit kannaber nicht nur auf die gesamte Liste, sondern auch auf Listen mit bestimmtenWerten gematcht werden (siehe Listing 5.18).

Listing 5.18: checkValue mit Pattern Matching auf einen Wert und auf Typen

val checkLists: List[Any] => Unit = {case Nil =>println("eine leere Liste")

case List(_,2,_) =>println("eine dreielementige Liste"+

" mit einer 2 an Position 2")case List(_,_,_) =>println("eine dreielementige Liste")

case _ :: _ :: Nil =>println("eine zweielementige Liste")

case _ :: _ =>println("eine nichtleere Liste")

}

Da Typ-Parameter in Scala genauso wie in Java beim Übersetzen entfernt wer-den, ist es nicht möglich, mit einem Pattern bezüglich des Typ-Parameters zumatchen.8 In Listing 5.19 steht der Versuch, bei Listen von Strings etwas anderesauszugeben wie bei Listen von Ints.

Listing 5.19: Beispiel für Pattern Matching, das sich aufgrund von Type Erasure unerwartetverhält

// Achtung: Type Erasureval doesNotWorkAsExpected: List[Any] => Unit = {case _: List[String] =>println("eine Liste mit Strings")

case _: List[Int] =>println("eine Liste mit Ints")

}

7 Der Wert Nil steht für die leere Liste.8 Mehr Informationen zum Typsystem und zur Type Erasure erwarten Sie in Abschnitt 5.7.

Page 131: Scala : objektfunktionale Programmierung

118 5 Funktionales Programmieren

Wie die folgende Sitzung zeigt, wird der zweite Fall nie erreicht, weil der Typ-Parameter zur Laufzeit nicht mehr vorhanden ist und beide Muster dann auf jedebeliebige Liste passen:

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

scala> doesNotWorkAsExpected(listOfInts)eine Liste mit Strings

Wird die Scala-Shell mit dem Flag -unchecked gestartet, sehen wir auch die ent-sprechenden Warnungen:

<console>:6: warning: non variable type-argument Stringin type pattern List[String] is unchecked since it iseliminated by erasure

case _: List[String] =>^

<console>:8: warning: non variable type-argument Int intype pattern List[Int] is unchecked since it iseliminated by erasure

case _: List[Int] =>^

Ist es nur wichtig zu wissen, ob das erste Element ein String ist, kann dasPattern wie im ersten Fall von Listing 5.4 aufgebaut sein. Allerdings passt dasPattern dann auch beispielsweise auf die Liste List("Hallo",1,true,3.4):List[Any].

val worksAsExpected: List[Any] => Unit = {case (x:String)::_ =>println("eine Liste mit einem String als erstes Element"

)case _ => println("eine andere Liste")

}

Natürlich kann auch auf Tupel gematcht werden, wie das folgende Listing 5.20zeigt.

Listing 5.20: checkValue mit Pattern Matching auf Tupel

val checkValue: Any => Unit = {case (1,"Hallo") =>println("ein Tupel mit einer 1 und \"Hallo\"")

case (_,1.2) => println("Ein Tupel mit dem Wert"+" 1.2 an zweiter Stelle")

case (_,_,_) => println("Ein Tripel")case i: Int => println("ein Int: "+i)case _ => println("etwas anderes")

}

Page 132: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 119

Zusätzlich zum Pattern können noch sogenannte Guards angegeben werden. EinGuard ist ein boolescher Ausdruck, der mit if eingeleitet wird. Wenn das Pat-tern passt, wird der dazugehörige Guard ausgewertet. Nur wenn dessen Ergebnistrue ist, wird der Code hinter dem Pfeil ausgeführt.

Listing 5.21: Pattern Matching mit Guards

val checkValue: Any => Unit = {case (x:Int,y:Int) if x*y > 0 =>println("gleiche Vorzeichen")

case (_:Int,_:Int) =>println("verschiedene Vorzeichen")

case _ => println("kein Int-Tupel")}

In Listing 5.21 wird ein Guard verwendet, um zu überprüfen, ob die beiden Kom-ponenten des Tupels das gleiche Vorzeichen haben. Vorher wird durch das Patternsichergestellt, dass es sich bei beiden Komponenten um Ints handelt.

5.4.1 Case-Klassen

Bisher haben wir nur Pattern mit vordefinierten Typen verwendet. Pattern Mat-ching ist aber auch mit Objekten selbst definierter Klassen möglich und sogar mitsehr wenig zusätzlichem Aufwand. Wir müssen nur eine Case-Klasse durch Voran-stellen des Schlüsselwortes case definieren. Listing 5.22 zeigt ein einfaches Bei-spiel.

Listing 5.22: Definition der Case-Klasse Movie

case class Movie(title: String, fsk: Int)

Durch die Definition der Klasse Movie als Case-Klasse ist es möglich, auf ein Mus-ter der Form Movie(t,f) mit einem String t und einem Int f zu matchen.Listing 5.23 zeigt die Funktion printAllowedAtAgeOf, die den Movie mittelsPattern Matching in seine Komponenten zerlegt und dabei unterscheidet, ob dieFSK-Zahl eine 0 ist oder nicht.

Listing 5.23: Die Funktion printAllowedAtAgeOf mit Pattern Matching für Movie-Objekte

val printAllowedAtAgeOf: Movie => Unit = {case Movie(t,0) =>println(t+" (keine Altersbeschraenkung)")

case Movie(t,i) => println(t+" (frei ab "+i+")")}

Page 133: Scala : objektfunktionale Programmierung

120 5 Funktionales Programmieren

Die Nutzung sieht dann wie folgt aus:

scala> val movie = Movie("Am Limit",0)movie: Movie = Movie(Am Limit,0)

scala> printAllowedAtAgeOf(movie)Am Limit (keine Altersbeschraenkung)

scala> printAllowedAtAgeOf(new Movie("Matrix",16))Matrix (frei ab 16)

Die Funktion printAllowedAtAgeOf funktioniert also wie erwartet. Werfenwir aber noch einmal einen Blick auf die ersten beiden Zeilen der obigen Sitzung,so fallen uns zwei Dinge auf: Erstens wir haben den Movie ohne new erzeugt,und die Ausgabe in der zweiten Zeile sieht auch nicht wie sonst aus.Das alles verdanken wir dem Schlüsselwort case vor der Klassendefinitionvon Movie. Der Compiler ermöglicht damit nicht nur Pattern Matching, son-dern generiert auch kanonische Implementierungen von toString, equalsund hashCode. Außerdem werden die Klassen-Parameter automatisch zu val-Feldern. Bei Bedarf kann aber explizit var vorangestellt werden. Das heißt, wirkönnen mit dem Movies sofort einiges anfangen, wie die folgende Sitzung zeigt:

scala> println(movie.title)Am Limit

scala> if (movie == Movie("Am Limit",0))| println("der selbe Film")

der selbe Film

scala> print(movie)Movie(Am Limit,0)

Darüber hinaus erzeugt der Compiler automatisch ein Companion-Objekt miteiner apply- und einer unapply-Methode. Durch die Factory-Methode applyist es möglich, Movie("Am Limit",0) statt new Movie("Am Limit",0) zuschreiben. Die unapply-Methode ist eine sogenannte Extraktor-Methode und wirdfür das Pattern Matching in Scala benötigt. Mit unapply wird ein Movie-Objektin seine Bestandteile in Form eines Tupels der Klassen-Parameter zerlegt:

scala> Movie.unapply(movie)res8: Option[(String, Int)] = Some((Am Limit,0))

Achtung: Die automatisch generierten Methoden beziehen sich alle auf dieKlassen-Parameter. Es ist natürlich trotzdem möglich, weitere Felder und zusätz-liche Konstruktoren zu einer Case-Klasse hinzuzufügen.Im der obigen Sitzung taucht ein neuer Datentyp auf: Option[(String,Int)]mit dem Wert Some((Am Limit, 0)). Dieser Option-Typ wird in Scala immerdann verwendet, wenn das Ergebnis ein optionaler Wert ist, d.h. wenn auch etwas

Page 134: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 121

schief gehen kann.9 Allgemein hat Option[T] zwei mögliche Werte Some(x),wobei x ein Wert vom Typ T ist und den erfolgreichen Fall repräsentiert, und denWert None, der dem Fehlerfall entspricht.Der Option-Typ wird beispielsweise in der Scala Collections-Bibliothek verwen-det, wie die folgende Sitzung beispielhaft verdeutlichen soll:

scala> val cities =| Map("A" -> "Augsburg", "B" -> "Berlin")

cities: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map((A,Augsburg), (B,Berlin))

scala> cities get "A"res0: Option[java.lang.String] = Some(Augsburg)

scala> cities get "C"res1: Option[java.lang.String] = None

Nachdem wir eine Map erzeugt haben, gibt der Zugriff auf das Paar mit demSchlüssel "A" den Wert "Augsburg" eingebettet in Some zurück. Der versuchteZugriff auf das Paar mit dem Schlüssel "C", das nicht vorhanden ist, führt zumErgebnis None.Ab Scala 2.8.0 wird für Case-Klassen auch eine copy-Methode generiert, mit dereine Kopie eines Objektes erzeugt werden kann, z.B.

scala> val movie2 = movie.copy()movie2: Movie = Movie(Am Limit,0)

Dank der mit Scala 2.8 neu eingeführten named und default arguments (siehe Seite36) können einzelne Felder, auch wieder beschränkt auf die Klassen-Parameter,elegant mit einem anderen Wert belegt werden, z.B.

scala> val movie3 = movie.copy(title = "Biene Maja")movie3: Movie = Movie(Biene Maja,0)

Um Laufzeitfehler zu vermeiden, ist es wichtig, beim Pattern Matching alle mög-lichen Fälle zu beachten. Wird ein Fall nicht behandelt, wird bei Datentypen ausder Scala-Standard-Bibliothek eine entsprechende Warnung ausgegeben:

scala> val errorOnEmptyList: (List[Any]) => Any = {| case x::_ => x| }

<console>:6: warning: match is not exhaustive!missing combination NilerrorOnEmptyList: (List[Any]) => Any = <function1>

Diese hilfreiche Warnung taucht natürlich nicht auf, sobald ein Default-Fall mitdem Unterstrich definiert wird:

9 In Java werden für diese Art von Fehlern üblicherweise null-Werte verwendet.

Page 135: Scala : objektfunktionale Programmierung

122 5 Funktionales Programmieren

scala> val throwExceptionOnEmptyList| : (List[Any]) => Any = {| case x::_ => x| case _ => throw new Exception| }

throwExceptionOnEmptyList: (List[Any]) => Any =<function1>

Im folgenden Abschnitt zeigen wir Ihnen, wie wir eine solche Warnung auch fürselbst definierte Klassen bekommen können.

5.4.2 Versiegelte Klassen

Wir definieren zunächst eine abstrakte Klasse Lecture für Lehrveranstaltungen.Bei den Lehrveranstaltungen unterscheiden wir zwischen Vorlesungen, Übungenund von Studenten gehaltenen Tutorien. Um Pattern Matching zu nutzen, definie-ren wir die drei Subklassen Course, Exercise und Tutorial als Case-Klassen.Die Implementierung der vier Klassen ist in Listing 5.24 wiedergegeben.

Listing 5.24: Die Klassen Lecture, Course, Exercise und Tutorial

abstract class Lecturecase class Course(title: String) extends Lecturecase class Exercise(belongsTo: Course) extends Lecturecase class Tutorial(belongsTo: Course) extends Lecture

Anschließend können wir die Funktion courseTitle (wie in Listing 5.25 ge-zeigt) mit Pattern Matching definieren.

Listing 5.25: Die Funktion courseTitle

def courseTitle(lecture: Lecture) = {lecture match {case Course(title) => titlecase Exercise(Course(title)) => titlecase Tutorial(Course(title)) => title

}}

Definieren wir nur Pattern für Course und Exercise, so kann die Funktiontrotzdem ohne Fehlermeldung übersetzt werden:

scala> def courseTitle(lecture: Lecture) = {| lecture match {| case Course(title) => title| case Exercise(Course(title)) => title| }| }

courseTitle: (lecture: Lecture)String

Page 136: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 123

Die Anwendung der Funktion auf ein Tutorial führt dann aber zu einemMatchError, wie die folgende Sitzung zeigt:

scala> courseTitle(Tutorial(Course(| "Fortgeschrittene funktionale Programmierung")))

scala.MatchError: Tutorial(Course(Fortgeschrittenefunktionale Programmierung))

at .courseTitle(<console>:11)...

Der Scala-Compiler kann ohne zusätzliche Information auch nicht warnen, dassein Fall beim Matching in der Funktion courseTitle vermutlich vergessen wur-de, da es in einer objektorientierten Sprache grundsätzlich möglich ist, eine wei-tere Klasse von Lecture abzuleiten.Die Erzeugung weiterer Subklassen von Lecture kann aber unterbunden wer-den. Damit kann der Compiler die gewünschte Warnung ausgeben. Dazu müssenwir die Klasse Lecture mit dem Schlüsselwort sealed versiegeln. Für eine SealedClass gilt, dass alle davon abgeleiteten Klassen in derselben Sourcecode-Datei wiedie versiegelte Klasse stehen müssen. Also definieren wir die vier Klassen wie in5.26 dargestellt in einer Datei.

Listing 5.26: Die versiegelte Klasse Lecture und die davon abgeleiteten Case-KlassenCourse, Exercise und Tutorial

// in einer gemeinsamen Sourcecode-Dateisealed abstract class Lecturecase class Course(title: String) extends Lecturecase class Exercise(belongsTo: Course) extends Lecturecase class Tutorial(belongsTo: Course) extends Lecture

Die anschließende Definition von courseTitle nur für Course und Exerciseliefert dann die gewünschte Warnung:

scala> def courseTitle(lecture: Lecture) = {| lecture match {| case Course(title) => title| case Exercise(Course(title)) => title| }| }

<console>:11: warning: match is not exhaustive!missing combination Tutorial

lecture match {^

courseTitle: (lecture: Lecture)String

Allerdings sind die Möglichkeiten des Compilers bei versiegelten Klassen nur aufdie direkt abgeleiteten Case-Klassen beschränkt. Definieren wir courseTitle,wie in der folgenden Sitzung zu sehen, bekommen wir keine Warnung, könnenaber dennoch einen MatchError verursachen:

Page 137: Scala : objektfunktionale Programmierung

124 5 Funktionales Programmieren

scala> def courseTitle(lecture: Lecture) = {| lecture match {| case Course(title) => title| case Exercise(Course(title)) => title| case Tutorial(Course("Prog 2")) => "Prog 2"| }| }

courseTitle: (lecture: Lecture)String

scala> courseTitle(Tutorial(Course("Prog 1")))scala.MatchError: Tutorial(Course(Prog 1))

at .courseTitle(<console>:13)...

Verwenden wir zwar eine versiegelte Klasse, wollen aber bewusst einzelne Fälleauslassen, z.B. weil wir wissen, dass sie nicht vorkommen können, können wirdies dem Compiler mit der @unchecked-Annotation mitteilen. Wir bekommenin dem Fall keine Warnung mehr. Zum Beispiel würden wir bei der folgendenFunktion ohne die @unchecked-Annotation eine Warnung bekommen, dass derFall Nil fehlt. Mit der Annotation können wir die Funktion ohne Warnung über-setzen:

def nonEmptyListMatch(l: List[Any]) =(l: @unchecked) match {case 1::_ => println("Liste beginnt mit der 1")case _::_ => println("Liste beginnt nicht mit 1")

}

5.4.3 Partielle Funktionen

Wenn wir, wie im vorherigen Abschnitt gezeigt, zwar alle Case-Klassen abdecken,aber innerhalb der Klassen nicht alle Fälle, so können wir courseTitle als par-tielle Funktion10 definieren, wie in Listing 5.27 dargestellt.

Listing 5.27: Die partielle Funktion courseTitle

def courseTitle: PartialFunction[Lecture,String] = {case Course(title) => titlecase Exercise(Course(title)) => titlecase Tutorial(Course("Prog 2")) => "Prog 2"

}

Die PartialFunction hat zwei Typ-Parameter: den Argumenttyp und den Er-gebnistyp. Im obigen Listing wird also eine partielle Funktion von Lecture nachString definiert. Nutzen wir die partielle Funktion wie die zuvor definierte

10 Eine Funktion, die nicht für alle Werte ihres Definitionsbereichs definiert ist, heißt partielle (alsopartiell definierte) Funktion.

Page 138: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 125

Funktion courseTitle, so können wir damit immer noch einen MatchErrorerzeugen:

scala> courseTitle(Tutorial(Course("Prog 1")))scala.MatchError: Tutorial(Course(Prog 1))

at .courseTitle(<console>:13)...

Es ist aber möglich, mit der Methode isDefinedAt dynamisch (also zur Lauf-zeit) zu überprüfen, ob die partielle Funktion an der entsprechenden Stelle defi-niert ist, bevor wir unwissentlich in den MatchError laufen:

scala> courseTitle.isDefinedAt(| Tutorial(Course("Prog 1")))

res5: Boolean = false

Der Trait PartialFunction definiert noch einige andere nützliche Methoden:

andThen komponiert die partielle Funktion mit einer Funktion, die das Er-gebnis transformiert, z.B.

scala> (courseTitle andThen (_.map(_.toUpper)))(| Course("Programmierung 2"))

res6: String = PROGRAMMIERUNG 2

orElse komponiert die partielle Funktion mit einer Funktion, die berechnetwird, wenn die partielle Funktion für das Argument nicht definiert ist, z.B.

scala> val unknown:PartialFunction[Lecture,String] = {| case _ => "unknown title"| }

unknown: PartialFunction[Lecture,String] = <function1>

scala> (courseTitle orElse unknown)(| Course("Programmierung 2"))

res7: String = Programmierung 2

scala> (courseTitle orElse unknown)(| Tutorial(Course("Programmierung 2")))

res8: String = unknown title

lift macht aus der partiellen Funktion eine totale Funktion, die für definierteWerte x das Ergebnis Some(x) und für nicht definierte Werte das ErgebnisNone zurückliefert, z.B.

scala> (courseTitle lift)(Course("Prog 1"))res9: Option[String] = Some(Prog 1)

scala> (courseTitle lift)(Tutorial(Course("Prog 1")))res10: Option[String] = None

Page 139: Scala : objektfunktionale Programmierung

126 5 Funktionales Programmieren

5.4.4 Variablennamen für (Teil-)Pattern

In manchen Fällen wird ein Objekt mit einem Pattern zerlegt, aber das gesamteObjekt auf der rechten Seite des Doppelpfeils wieder genutzt. Um nicht so etwaswie

case Tutorial(Course("Prog 2")) =>"In diesem Semester: Tutorial zur Vorlesung "+Course("Prog2")

schreiben zu müssen, bei dem das Course("Prog 2") der linken Seite aufder rechten Seite wiederholt wird, kann mit dem Pattern Tutorial(course @Course("Prog 2")) der Name course für das Teil-Pattern vergeben werden,sodass statt oben angegebener Zeile

case Tutorial(course @ Course("Prog 2")) =>"In diesem Semester: Tutorial zur Vorlesung "+course

geschrieben werden kann.

5.4.5 Exception Handling

Auch wenn Exception Handling (Ausnahmebehandlung) nicht der funktionalen Pro-grammierung entstammt, passt es gut in den Abschnitt über Pattern Matching.Müssen wir in einem Code-Block damit rechnen, dass eine Exception geworfenwerden kann, z.B. weil wir versuchen, eine Datei zu öffnen, umschließen wir denBlock mit try und reagieren in dem darauf folgendem Catch-Block auf die mögli-chen Ausnahmen. Im Gegensatz zu anderen Programmiersprachen wie Java, diefür verschiedene Exceptions mehrere Catch-Blöcke vorsehen, wird in Scala in ei-nem einzigen Catch-Block mittels Pattern Matching zwischen den Exceptions un-terschieden.Listing 5.28 zeigt ein Beispiel, bei dem eine Zahl von der Kommandozeile einge-lesen werden soll. Gibt der Benutzer eine Zeichenkette ein, die sich nicht in einenInt umwandeln lässt, so wird eine Exception geworfen. Diese wird dann in demCatch-Block abgefangen.

Listing 5.28: Exception-Handling in der Funktion readIntOption

def readIntOption() = {try {Some(readInt)

} catch {case _: NumberFormatException => None

}}

Im Try-Block können wir davon ausgehen, dass alles gut geht, und den Wert,den readInt einliest, in ein Some-Objekt einpacken. Geht etwas schief, wird

Page 140: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 127

der Block verlassen und der Catch-Block betreten. In diesem wird die Ex-ception mit Pattern untersucht. Trifft das Pattern zu, wird der entsprechendeCode ausgeführt. Passt kein Pattern, wird die Exception nicht behandelt undweiter geworfen. In Listing 5.28 gibt es nur ein einziges Pattern, auf das ei-ne NumberFormatException passt. Alle anderen Exceptions bleiben von derFunktion readIntOption unbehandelt.Für das Behandeln verschiedener Exceptions werden keine zusätzlichen Catch-Blöcke benötigt. Durch das Pattern Matching können mehrere Exceptions in ei-nem Block behandelt werden. Listing 5.29 zeigt eine alternative Implementie-rung der Funktion readIntOption, bei der bei allen Exceptions außer derNumberFormatException eine zuvor selbst definierte ReadIntExceptiongeworfen wird.

Listing 5.29: Unterschiedliches Handling von Exceptions in readIntOption

case object ReadIntException extendsException("unexpected error in readIntOption")

def readIntOption() = {try {Some(readInt)

} catch {case _: NumberFormatException => Nonecase _ => throw ReadIntException

}}

Nachdem das Objekt ReadIntException als Case-Objekt definiert wurde, kannvom Aufrufer darauf gematcht werden. Es gibt außerdem auch einen Finally-Block, der immer ausgeführt wird, egal ob der Try-Block problemlos abgearbei-tet werden konnte oder eine Exception geworfen wurde. Bei der Exception ist esauch ohne Belang, ob sie in einem Catch-Block gefangen wurde oder nicht. DerFinally-Block schließt sich mit dem Schlüsselwort finally an den Catch-Blockoder, falls nicht vorhanden, an den Try-Block an. Ein try ohne catch mit einemfinally ist dann sinnvoll, wenn keine Exception behandelt werden soll, abereine oder mehrere Anweisungen wie z.B. das Schließen eines Dateihandles unbe-dingt ausgeführt werden sollen, bevor die Funktion beendet wird.

Listing 5.30: Die Funktion readIntOption mit try, catch und finally

def readIntOption() = {try {Some(readInt)

} catch {case _: NumberFormatException => Nonecase _ => throw ReadIntException

} finally {

Page 141: Scala : objektfunktionale Programmierung

128 5 Funktionales Programmieren

println("readIntOption beendet")}

}

Listing 5.30 zeigt ein Beispiel, in dem am Ende der Funktion immer auf der Con-sole readIntOption beendet ausgegeben wird und zwar, bevor Some, Noneoder die Exception weitergegeben wird.Scala kennt keinen Unterschied zwischen sogenannten checked und unchecked ex-ceptions. Java macht aber diese Unterscheidung und verlangt, das Checked Ex-ceptions in der Signatur einer Methode angegeben werden. Dies ist wiederum inScala zunächst gar nicht möglich. Wird aber in Java eine Scala-Methode genutzt,die eine Checked Exception werfen kann, und hat die Java-Methode einen Catch-Block dafür, wird der Java-Compiler einen Fehler anzeigen, der besagt, dass dieException gar nicht geworfen wird – sonst würde sie ja in der Signatur stehen.Der Ausweg in Scala ist die @throws-Annotation, die die Klasse der Exceptionals Argument hat, z.B.

class Reader(fname: String) {private val in = new BufferedReader(new FileReader(

fname))@throws(classOf[IOException])def read() = in.read()

}

Wenn die Chance besteht, dass Ihr Scala-Code aus Java heraus genutzt werdensoll, sollten Sie sich angewöhnen, die @throws-Annotation zu nutzen. Innerhalbvon Scala ist sie unnötig.

5.4.6 Extraktoren

In Abschnitt 5.4.1 haben wir Case-Klassen und ihre zahlreichen Vorteile erläutert.Case-Klassen haben aber auch einen Nachteil: Sie machen ihre konkrete Datenre-präsentation explizit. Wenn es nur um die Eleganz beim Pattern Matching geht,müssen wir keine Case-Klasse nutzen. Wir können stattdessen ein Objekt mit ei-ner unapply-Methode implementieren. Ein Objekt mit einer unapply-Methodenennen wir Extraktor.Listing 5.31 zeigt das Extraktor-Objekt HeaderField, das ein Schlüssel-Wert-Paar aus einem E-Mail-Header-Feld extrahiert, z.B. aus der Zeichenkette From:[email protected] das Tupel (From,[email protected]).

Listing 5.31: Der Extraktor HeaderField

object HeaderField {def unapply(s: String) = {val (key,value) = s.span(_!=’:’)if (key.isEmpty || value.isEmpty)

Page 142: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 129

NoneelseSome((key,value.tail trim))

}}

Die unapply-Methode versucht, die Zeichenkette beim ersten Vorkommen desZeichens : in zwei Teile zu zerlegen. Ist eines der beiden Teile leer, so war derDoppelpunkt das erste oder letzte Zeichen in der Zeile oder kam überhaupt nichtvor. Andernfalls werden vom zweiten Teil das erste Zeichen, der Doppelpunkt,und anschließend alle Leerzeichen am Anfang und am Ende entfernt.In Listing 5.32 ist die Klasse Email dargestellt. Ein Email-Objekt besteht auseiner Map mit Paaren aus dem Namen eines Headerfeldes und dem dazugehö-rigen Wert und aus einem String, der den Text der E-Mail repräsentiert. DietoString-Methode ist zu Demonstrationszwecken redefiniert (siehe Scala-Shell-Sitzung weiter unten).

Listing 5.32: Die Klasse Email

class Email(val header: Map[String,String],val body: String) {

override def toString: String = header+"\n"+body}

Die Funktion stringToEmail (siehe Listing 5.33) nutzt den Extractor Header-Field, um mittels Pattern Matching eine Headerzeile in ihre Bestandteile zu zer-legen. Dazu wird der Parameter s zuerst mit lines.toList in eine Liste vonZeilen zerlegt. Anschließend wird die Liste in zwei Teile zerlegt. Die leere Zei-chenkette dient dabei als Trenner, denn zwischen Header und Body einer E-Mailist eine Leerzeile.Mit der Header- und Bodyliste erzeugen wir ein neues Email-Objekt, wobei wirdie beiden Listen noch in eine Map bzw. einen String umwandeln müssen. DieListe der Headerfelder bearbeiten wir mit einem Fold von rechts nach links miteiner leeren Map als Startwert. Die Funktion zum Falten nimmt eine Zeile und diebisher berechnete Map. Kann die Zeile mit dem HeaderField-Extraktor in einSchlüssel-Wert-Paar zerlegt werden, wird aus dem Paar und der Map mit der Map-Methode + eine neue11 Map erzeugt. Die Liste, die den E-Mail-Body repräsentiert,enthält noch die führende Leerzeile. Diese wird entfernt12, und aus der Liste vonZeilen wird wieder ein einziger String mit Zeilenumbrüchen erzeugt.

11 Die ohne speziellen Import verwendete Map ist scala.collection.immutable.Map, also un-veränderbar.12 Genau genommen wird auch hier wieder eine neue Liste ohne die führende Leerzeile berechnet.

Page 143: Scala : objektfunktionale Programmierung

130 5 Funktionales Programmieren

Listing 5.33: Die Funktion stringToEmail

implicit def stringToEmail(s: String) = {val (headLines,body) = s.lines.toList span (_!="")new Email((headLines :\ Map[String,String]())((field,map) =>field match {case HeaderField(k,v) => (map + (k -> v))case _ => map

}),body.tail mkString "\n"

)}

Nachdem die Funktion stringToEmail mit dem Schlüsselwort implicit mar-kiert wurde, müssen wir die Funktion nicht einmal explizit anwenden, sondernkönnen einen String einer Variablen vom Typ Email zuweisen. Nachdem dieszu einem Typfehler führt, darf der Compiler alle impliziten Funktionen überprü-fen, ob genau eine davon in der Lage ist, den String in eine Email zu transfor-mieren. Die folgende Sitzung zeigt dies im Beispiel:

scala> val email: Email = """From: [email protected]| |Subject: Hallo Leser| || |Viel Spass mit Scala.| || |Gruss| |Oliver Braun""".stripMargin

email: Email =Map(Subject -> Hallo Leser, From -> [email protected])Viel Spass mit Scala.

GrussOliver Braun

5.4.7 Pattern Matching mit regulären Ausdrücken

Scala unterstützt wie viele moderne Programmiersprachen auch reguläre Aus-drücke. Mit regulären Ausdrücken können Zeichenketten zerlegt werden. Ein re-gulärer Ausdruck passt auf eine Zeichenkette oder er passt nicht. Insofern ist erden bisherigen Pattern sehr ähnlich. Daher können reguläre Ausdrücke in Scalagenau wie Pattern genutzt werden.Um einen regulären Ausdruck zu erzeugen, kann die Klasse scala.util.matching.Regex genutzt werden. Diese steht nicht über das Predef-Objektzur Verfügung, sondern muss explizit importiert werden. In der folgenden Sit-zung wird ein regulärer Ausdruck für ein Datum in der Form Tag.Monat.Jahr

Page 144: Scala : objektfunktionale Programmierung

5.4 Case-Klassen und Pattern Matching 131

erzeugt, wobei der Tag und der Monat ein- oder zweistellig sein dürfen und dasJahr zwei- oder vierstellig sein muss:

scala> import scala.util.matching.Regeximport scala.util.matching.Regex

scala> val dateRegex = new Regex(| """(\d{1,2})\.(\d{1,2}).(\d{2}|\d{4})""")

dateRegex: scala.util.matching.Regex = (\d{1,2})\.(\d{1,2}).(\d{2}|\d{4})

Die Syntax für den regulären Ausdruck entspricht der Java-Syntax und die wie-derum der Perl-Syntax für reguläre Ausdrücke. Ein regulärer Ausdruck kannauch mit der Methode r direkt aus einem String erzeugt werden. Das heißt,statt der obigen Sitzung kann auch geschrieben werden:

scala> val dateRegex =| """(\d{1,2})\.(\d{1,2}).(\d{2}|\d{4})""".r

dateRegex: scala.util.matching.Regex = (\d{1,2})\.(\d{1,2}).(\d{2}|\d{4})

Der String für den regulären Ausdruck muss mit jeweils drei doppelten Anfüh-rungszeichen umschlossen werden, da sonst beispielsweise \d als Steuerzeicheninterpretiert wird. Natürlich ist es auch möglich, alles entsprechend zu maskierenund nur ein paar Anführungszeichen zu nehmen. Damit wird die Zeichenketteaber etwas unübersichtlicher:

scala> val dateRegex =| "(\\d{1,2})\\.(\\d{1,2}).(\\d{2}|\\d{4})".r

dateRegex: scala.util.matching.Regex = (\d{1,2})\.(\d{1,2}).(\d{2}|\d{4})

Der dateRegex gruppiert die Zahlen für den Tag, den Monat und das Jahr mitden runden Klammern. Es gibt also drei Gruppen in dem regulären Ausdruck,die für das Pattern genutzt werden können. In Listing 5.34 ist ein Beispiel für eineFunktion angegeben, die überprüft, ob eine Zeichenkette einem Datum entspricht.Ist dies der Fall, wird ein Tupel zurückgegeben, das aus Tag, Monat und Jahr be-steht. Um im Fehlerfall angemessen reagieren zu können, verwendet die Funktionden Typ Option für das Ergebnis.

Listing 5.34: Die Funktion extractDate zum Zerlegen einer Zeichenkette mit PatternMatching und einem regulären Ausdruck

def extractDate(str: String): Option[(Int,Int,Int)] = {val dateRegex ="""(\d{1,2})\.(\d{1,2}).(\d{2}|\d{4})""".r

str match {case dateRegex(d,m,y) =>Some((d.toInt,m.toInt,y.toInt))

Page 145: Scala : objektfunktionale Programmierung

132 5 Funktionales Programmieren

case _ => None}

}

5.5 Currysierung und eigene Kontrollstrukturen

Wir haben auf Seite 98 im Zusammenhang mit den impliziten Parametern bereitsdie Currysierung angesprochen. Currysierung ist die Umwandlung einer Funkti-on mit mehreren Parametern in eine Kette von Funktionen mit je einem Parame-ter. Die Currysierung ist benannt nach dem Logiker und Mathematiker HaskellBrooks Curry, nach dessen Vorname übrigens die rein funktionale Programmier-sprache Haskell benannt wurde.Wir wollen zunächst auch das klassische Beispiel für die Erläuterung der Curry-sierung verwenden: die Funktion add zum Addieren zweier ganzer Zahlen. Dienicht currysierte Version von add könnte wie in Listing 5.35 aussehen.

Listing 5.35: Die nicht currysierte Funktion add

def add(x: Int, y: Int) = x + y

Ein mögliche Nutzung der Funktion add wäre dann zum Beispiel:

scala> add(1,2)res0: Int = 3

Currysieren wir nun add, so wird daraus eine Funktion mit nur einem Parameter,wie in Listing 5.36 dargestellt.

Listing 5.36: Die currysierte Funktion add

def add(x: Int) = (y: Int) => x + y

Wenden wir nun die currysierte Version auf eine Zahl an, erhalten wir eine Funk-tion, die wir dann auf eine zweite Zahl anwenden können:

scala> val add1 = add(1)add1: (Int) => Int = <function1>

scala> add1(2)res1: Int = 3

Die currysierte Funktion add muss nicht mit einem Funktionsliteral auf der rech-ten Seite definiert werden. Scala erlaubt die Angabe mehrerer Parameterlisten.Das heißt, üblicherweise wird die Funktion dann wie in Listing 5.37 implemen-tiert.

Page 146: Scala : objektfunktionale Programmierung

5.5 Currysierung und eigene Kontrollstrukturen 133

Listing 5.37: Die currysierte Funktion add mit zwei Parameterlisten

def add(x: Int)(y: Int) = x + y

Die Anwendung der in Listing 5.37 gezeigten Funktion ist dann genauso wie dieder Funktion aus Listing 5.36. Es ist außerdem mit beiden auch möglich, sofortbeide Parameterlisten anzugeben, z.B.

scala> add(1)(2)res2: Int = 3

Currysierte Funktionen können auch aus nicht-currysierten erzeugt werden. Da-zu gibt es die Methode curried in allen Functionn Traits für n ≥ 2. Betrachtenwir noch einmal die nicht currysierte Funktion add aus Listing 5.35. Dann könnenwir die eine currysierte Funktion definieren durch:

def addCurried = (add _).curried

Die so definierte Funktion addCurried kann nun auf zwei Parameterlisten an-gewendet werden:

scala> addCurried(2)(3)res3: Int = 5

Interessant sind solche currysierten Funktionen beispielsweise bei der Verwen-dung in Funktionen höherer Ordnung, wie in der folgenden Sitzung dargestellt:

scala> val add1 = addCurried(1)add1: (Int) => Int = <function1>

scala> (1 to 10).toList map add1res4: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11)

Das heißt, hier müssen keine Platzhalter für die Parameter angegeben werden,sondern nur der Bezeichner der Funktion selbst. Dies funktioniert auch für die ge-samte Parameterliste von nicht currysierten Funktionen. Der folgende Ausdruckberechnet die Summe aller Elemente einer Liste mit der nicht currysierten Funk-tion add aus Listing 5.35:

scala> ((1 to 10).toList :\ 0)(add)res5: Int = 55

Die Funktion add hat den Typ (Int,Int) => Int. Das lässt darauf schließen,dass es bei der Currysierung nicht um einzelne Parameter, sondern um ganze Pa-rameterlisten geht. Sehen wir aber eine Parameterliste als einzelnen Parameter an,so handelt es sich dabei einfach um ein Tupel. Somit passt die Umsetzung in Scalaauch wieder zur Theorie. Currysierung ist also nicht nur für einzelne Parameter,sondern auch für Parameterlisten möglich. Listing 5.38 zeigt die Definition derFunktion addTuple, die zwei Tupel komponentenweise addiert.

Page 147: Scala : objektfunktionale Programmierung

134 5 Funktionales Programmieren

Listing 5.38: Die Funktion addTuple mit zwei zweielementigen Parameterlisten

def addTuple(a: Int,b: Int)(c: Int,d:Int) = (a+c,b+d)

Die Funktion addTuple lässt sich dann folgendermaßen auf zwei „Tupel” an-wenden:

scala> addTuple(1,2)(3,5)res6: (Int, Int) = (4,7)

Das Wort Tupel steht im vorangehenden Satz in Anführungsstrichen, weil es sicheben nicht um Tupel, sondern um zwei Parameterlisten handelt. Eine Anwendungder Funktion addTuple auf zwei echte Tupel ist so nicht möglich:

scala> val t1 = (1,2)t1: (Int, Int) = (1,2)

scala> val t2 = (3,5)t2: (Int, Int) = (3,5)

scala> addTuple(t1)(t2)<console>:9: error: not enough arguments for method

addTuple: (a: Int,b: Int)(c: Int,d: Int)(Int, Int).Unspecified value parameter b.

addTuple(t1)(t2)^

Wir könnten entweder die Tupel bei der Anwendung der Funktion „auspacken”:

scala> addTuple(t1._1,t1._2)(t2._1,t2._2)res7: (Int, Int) = (4,7)

was den Code aber auch nicht gerade übersichtlicher werden lässt. Oder aber wirdefinieren addTuple mit zwei Parameterlisten, die jeweils ein Tupel enthalten,wie in Listing 5.39 dargestellt.

Listing 5.39: Die Funktion addTuple mit zwei Parameterlisten mit je einem Tupel

def addTuple(t1: (Int,Int))(t2: (Int,Int)) =(t1._1 + t2._1, t1._2 + t2._2)

Auf die Komponenten der beiden Tupel wird mit den Methoden _1 und _2 zu-gegriffen. Es ist auch möglich, die Tupel mithilfe von Pattern Matching in ihreBestandteile zu zerlegen (siehe Listing 5.40).

Listing 5.40: Die Funktion addTuple mit zwei Parameterlisten mit je einem Tupel undZerlegung durch Pattern Matching

def addTuple: ((Int,Int)) => ((Int,Int)) => (Int,Int) = {case (a,b) => {case (c,d) => (a+c,b+d)

Page 148: Scala : objektfunktionale Programmierung

5.5 Currysierung und eigene Kontrollstrukturen 135

}}

Die für Tupel definierte Funktion addTuple kann dann wirklich auf Tupel ange-wendet werden, wie die folgende Sitzung illustrieren soll:

scala> val t1 = (1,2)t1: (Int, Int) = (1,2)

scala> val t2 = (3,5)t2: (Int, Int) = (3,5)

scala> addTuple(t1)(t2)res8: (Int, Int) = (4,7)

scala> addTuple((5,3))((8,3))res9: (Int, Int) = (13,6)

scala> addTuple(5,3)(8,3)res10: (Int, Int) = (13,6)

Wie oben zu sehen ist, müssen bei der Anwendung der Funktion auf zwei Tupeldie doppelten Klammernpaare, ein Paar für die Tupelbildung und ein Paar für dieParameterliste, nicht geschrieben werden. Es reicht ein Paar von Klammern. Beider Definition der Funktion in Listing 5.40 müssen im Typ die doppelten Klam-mernpaare angegeben werden, sonst wäre es eine Parameterliste, bestehend auszwei Ints und nicht aus einem Tupel.Was wir bisher bezüglich Currysierung gezeigt haben, lässt sich mit dem Unter-strich fast genauso elegant lösen, z.B.

scala> def add(x: Int, y: Int) = x + yadd: (x: Int,y: Int)Int

scala> def add1 = add(1,_: Int)add1: (Int) => Int

scala> (1 to 10).toList map add1res11: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11)

Wird im Kontext eine Funktion erwartet, so kann der Unterstrich sogar weggelas-sen werden, wie in der letzten Eingabezeile obiger Sitzung zu sehen ist.Currysierung hat aber noch eine für die skalierbare Sprache Scala ganz entschei-dende Eigenschaft: Es lassen sich mit ihrer Hilfe Kontrollstrukturen definieren,die wie eingebaute Kontrollstrukturen aussehen.Ein Detail, das Sie dazu noch wissen müssen, ist die Möglichkeit, Parameterlistenmit genau einem Parameter mit geschweiften statt runden Klammern umschlie-ßen zu können. Wir können also schreiben:

scala> addTuple { (5,3) } { (8,3) }res12: (Int, Int) = (13,6)

Page 149: Scala : objektfunktionale Programmierung

136 5 Funktionales Programmieren

aber nicht

scala> add { 1, 2 }<console>:1: error: ’;’ expected but ’,’ found.

add { 1, 2 }^

Betrachten wir nun die in Listing 5.41 definierte Funktion printTupleAddedTozur Ausgabe der komponentenweisen Summe von zwei Tupeln.

Listing 5.41: Definition der printTupleAddedTo-Kontrollstruktur

def printTupleAddedTo(t1: (Int,Int))(t2: (Int,Int)) =println(t1+" + "+t2+" = "+

(t1._1 + t2._1, t1._2 + t2._2))

Diese Funktion können wir dann beispielsweise, wie in Listing 5.42 dargestellt, alsKontrollstruktur nutzen. Wichtig dabei ist, dass in der zweiten Parameterliste amEnde tatsächlich ein Tupel zurückgegeben wird. Daher ist die letzte Anweisungin diesem Fall (x,y).

Listing 5.42: Script zum Einlesen zweier Zahlen und Ausgabe nach der Addition mit einemTupel mit der selbst definierten printTupleAddedTo-Kontrollstruktur

printTupleAddedTo(1,2) {print("Geben Sie die erste Komponente ein: ")val x = readIntprint("Geben Sie die zweite Komponente ein: ")val y = readInt(x,y)

}

Durch das Ersetzen der runden durch geschweifte Klammern können wir nichtnur einen Wert als zweites Argument, sondern einen Code-Block übergeben, deraus mehreren Anweisungen besteht. Verwenden wir statt der geschweiften rundeKlammern, führt das zu folgendem Fehler:

scala> printTupleAddedTo(1,2) (| print("Geben Sie die erste Komponente ein: ")| val x = readInt

<console>:3: error: ’)’ expected but ’val’ found.val x = readInt^

Durch die Unterstützung von Funktionen höherer Ordnung ist es auch möglich,Kontrollstrukturen zu definieren, die Funktionen als Parameter haben. In Listing

Page 150: Scala : objektfunktionale Programmierung

5.5 Currysierung und eigene Kontrollstrukturen 137

5.43 ist eine Funktion definiert, die ein Prädikat13 und eine Zahl als Parameter hat.Erfüllt die Zahl das Prädikat, wird richtig sonst falsch ausgegeben.

Listing 5.43: Die Funktion guessNumber

def guessNumber(predicate: Int => Boolean)(number: Int) =if(predicate(number)) println("richtig") elseprintln("falsch")

Die Verwendung der Funktion guessNumber als Kontrollstruktur könnte dannaussehen, wie in 5.44 dargestellt. Das Prädikat wird als Funktionsliteral in rundenKlammern übergeben, die Zahl dann in einem Code-Block von der Kommando-zeile eingelesen.

Listing 5.44: Einlesen einer Zahl und Vergleich mit einem Prädikat

guessNumber (_ == 5) {print("Welche Zahl suchen wir? ")readInt

}

Als weiteres Beispiel wollen wir noch einmal die Klasse Email aus Abschnitt 5.4.6nutzen. In Listing 5.32 auf Seite 129 haben wir die Klasse definiert. Mit der Funk-tion stringToEmail (siehe Listing 5.33 auf Seite 130) und dem HeaderField-Extraktor (siehe Listing 5.31 auf Seite 128) haben wir einen String in ein Email-Objekt transformiert.Wir wollen uns nun eine eigene Kontrollstruktur email definieren, mit der wirein Email-Objekt erzeugen können. Der dem Bezeichner email folgende Code-Block soll nur die Headerfelder setzen. Außerdem wollen wir noch einen zweitenCode-Block mit dem Body übergeben können. Dazwischen soll der BezeichnerwithBody stehen. Das heißt, eine E-Mail mit leerem Body soll erzeugt werdendurch

email {// Headerfelder definieren

}

Eine E-Mail mit Header und Body durch

email {// Headerfelder definieren

} withBody {// Body definieren

}

13 Eine solche Funktion mit einem Wahrheitswert als Ergebnis wird üblicherweise als Prädikat be-zeichnet, da sie überprüft, ob ihr Parameter eine Bedingung erfüllt.

Page 151: Scala : objektfunktionale Programmierung

138 5 Funktionales Programmieren

Die Kontrollstruktur email ist in Listing 5.45 definiert durch das Companion-Objekt der Klasse Email mit der Methode email.

Listing 5.45: Das Email-Objekt mit der Methode email

object Email {def email(header: Map[String,String]) =new Email(header,"")

}

Die Erweiterung der email-Kontrollstruktur zur email-withBody-Kontroll-struktur ist in Listing 5.46 definiert. Die Methode bekommt einen body und gibtein neues Email-Objekt zurück.

Listing 5.46: Die Klasse Email mit der Methode withBody

class Email(val header: Map[String,String],val body: String) {

override def toString: String = header+"\n"+bodydef withBody(body: String) = new Email(header, body)

}

Um email statt Email.email schreiben zu können, muss die Methode einfachnur importiert werden. Listing 5.47 zeigt als Beispiel die Definition der FunktionstringToEmail mit der selbst definierten Kontrollstruktur.

Listing 5.47: Implementierung der Funktion stringToEmail mit der selbst definiertenemail-withBody-Kontrollstruktur

implicit def stringToEmail(s: String) = {import Email.emailval (headLines,body) = s.lines.toList span (_!="")email {(headLines :\ Map[String,String]())((field,map) =>field match {case HeaderField(k,v) => (map + (k -> v))case _ => map

})

} withBody {body.tail mkString "\n"

}}

Statt der Verwendung als Kontrollstruktur kann natürlich nach wie vor Email.email(...).withBody(...) geschrieben werden, da es sich ja um gewöhnli-che Methoden handelt.

Page 152: Scala : objektfunktionale Programmierung

5.5 Currysierung und eigene Kontrollstrukturen 139

Der funktionale Ansatz mit den Methoden email und withBody hat den Nach-teil, dass zwei Objekte erzeugt werden: das erste vom Companion-Objekt fürdie Methode email und das zweite von diesem Objekt selbst in der MethodewithBody. Dies ist ein typisches Beispiel, bei dem Abzuwägen ist, ob aus Perfor-mancegründen die Datenstruktur unveränderlich sein soll oder nicht.Eine mögliche Abweichung vom funktionalen Ansatz ist in Listing 5.48 darge-stellt.

Listing 5.48: Definition der Klasse Email mit einem veränderlichen Feld

class Email(val header: Map[String,String],private[this] var b: String) {

def body = boverride def toString: String = header+"\n"+bdef withBody(body: String) = {b = bodythis

}}

Neben der Notwendigkeit, sorgfältiger mit dem veränderlichen Email-Objektumgehen zu müssen, kann nun body nicht mehr als benannter Parameter ge-nutzt werden. Würde der Klassen-Parameter body genannt, kann keine Methodemit dem gleichen Namen definiert werden, um auf den Body zuzugreifen.Nachdem Kontrollabstraktionen häufig dazu genutzt werden, um Funktionen in-nerhalb der Kontrollstruktur auszuführen, benötigen wir noch einen Mechanis-mus, um Funktionen erst in der Kontrollstruktur auszuführen. Betrachten wirim Folgenden eine Funktion, die die Auswertung eines Ausdrucks so kapselt,dass sie bei einer beliebigen Exception None zurückgibt. Wird bei der Auswer-tung keine Exception geworfen, wird der berechnete Wert f als Some(f) zu-rückgegeben. Beispielsweise sollte optionCatch(8/2) den Wert Some(4) undoptionCatch(8/0) aufgrund der ArithmeticException den Wert None be-rechnen.Ein erster Ansatz ist in der folgenden Sitzung dargestellt:

scala> def optionCatch(f: Any) = {| try {| Some(f)| } catch {| case _ => None| }| }

optionCatch: (f: Any)Option[Any]

scala> optionCatch(8/2)res0: Option[Any] = Some(4)

Page 153: Scala : objektfunktionale Programmierung

140 5 Funktionales Programmieren

scala> optionCatch(8/0)java.lang.ArithmeticException: / by zero

at .<init>(<console>:7)...

Wie zu sehen ist, wird die Exception nicht in der Funktion optionCatch, son-dern außerhalb geworfen. Dies ist auch einleuchtend, denn in Scala werden al-le Funktionsargumente grundsätzlich vor dem Aufruf der Funktion ausgewertet.Dieses Verhalten nennt sich Call-by-Value.Ein einfacher Trick wäre, statt des Ausdrucks f eine Funktion zu übergeben.Durch die Angabe des Typs () => Any wird dies zum Ausdruck gebracht. DieFunktion wird dann übergeben und erst in der optionCatch-Funktion durchAnwendung auf die leere Parameterliste ausgewertet. Die folgende Sitzung zeigtdiesen Ansatz:

scala> def optionCatch(f: () => Any) = {| try {| Some(f())| } catch {| case _ => None| }| }

optionCatch: (f: () => Any)Option[Any]

scala> optionCatch(() => 8/2)res1: Option[Any] = Some(4)

scala> optionCatch(() => 8/0)res2: Option[Any] = None

Wunschgemäß berechnet die Funktion optionCatch für 8/2 den Wert Some(4)und für 8/0 den Wert None. Die Exception wird in optionCatch gefangen unddringt nicht nach außen. Äußerst unschön ist aber die Notwendigkeit, das Argu-ment als Funktionsliteral angeben zu müssen.Dies lässt sich aber durch die by-name-Parameter auch noch beheben. Dabei wirdim Vergleich zur obigen Definition einfach die leere Parameterliste weggelassen.Ein so definierter Parameter wird per Call-by-Name übergeben. Das bedeutet un-ausgewertet! Die folgende Sitzung zeigt die fertige optionCatch-Funktion miteinem By-Name-Parameter und die Anwendung der Funktion auf die Ausdrücke8/2 und 8/0:

scala> def optionCatch(f: => Any) = {| try {| Some(f)| } catch {| case _ => None| }| }

Page 154: Scala : objektfunktionale Programmierung

5.6 For-Expressions 141

optionCatch: (f: => Any)Option[Any]

scala> optionCatch(8/2)res3: Option[Any] = Some(4)

scala> optionCatch(8/0)res4: Option[Any] = None

Mit dieser Version von optionCatch kann nun beispielsweise folgender Codegeschrieben werden:

optionCatch {print("Numerator? ")val n = readIntprint("Denominator? ")val d = readIntn/d

} match {case Some(x) => println("Result is "+x)case None => println("An error occurred")

}

Egal ob nun die Eingabe eine Exception geworfen hat oder als Denominator eineNull eingegeben wurde, wird nun An error occurred ausgegeben.

5.6 For-Expressions

Auch wenn mit dem Schlüsselwort for üblicherweise imperative for-Schleifeneingeleitet werden, basieren diese in Scala auf einem mächtigeren Konstrukt, dasseine Wurzeln in der funktionalen Programmierung hat: der Comprehension. Com-prehension bedeutet, etwas zu traversieren, das mit einzuschließen (to compre-hend), was wir finden, und daraus etwas Neues zu berechnen. Die Ausdrückemit dem Schlüsselwort for werden daher in Scala als for-Comprehensions oder for-Expressions bezeichnet. Ein einfacher for-Ausdruck ist beispielsweise der folgen-de:

for (x <- 1 to 10) println(x)

Dieser Ausdruck gibt die Zahlen 1 bis 10 jeweils auf einer eigenen Zeile aus.Der Teilausdruck in Klammern heißt Generator, weil er die einzelnen Elementegeneriert, die beim Durchlauf genutzt werden. Ein Generator hat stets die Form<variablename> < − <collection>. Die damit lokal eingeführte Varia-ble entspricht einem val und ist daher unveränderbar. Der Teilausdruck 1 to10 erzeugt die Collection Range(1,2,3,4,5,6,7,8,9,10), die durchlaufenwird.

Page 155: Scala : objektfunktionale Programmierung

142 5 Funktionales Programmieren

Auch wenn es nicht anders als eine for-Schleife in einer beliebigen imperati-ven Programmiersprache aussieht14, handelt es sich eher um den mathematischenAusdruck ∀x ∈ {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} • println(x).Die for-Comprehensions von Scala lassen auch die direkte Angabe sogenann-ter Filter zu. Beispielsweise werden bei folgendem Ausdruck nur die ungeradenZahlen zwischen 1 und 10 ausgegeben:

for (x <- 1 to 10; if (x%2==1)) println(x)

Generator und Filter können auch jeweils auf einer eigenen Zeile angegeben wer-den. Dabei kann das Semikolon weggelassen werden:

for (x <- 1 to 10if (x%2==1)

) println(x)

Sind mehrere Bedingungen notwendig, können diese im Filter natürlich mit boo-leschen Operatoren verknüpft werden. Es ist aber auch möglich und zumeistübersichtlicher, mehrere getrennte Filter anzugeben. Im folgenden Beispiel wer-den nur die ungeraden und durch 5 teilbaren Zahlen zwischen 1 und 100 ausge-geben:

for (x <- 1 to 100if (x%2==1)if (x%5==0)

) println(x)

Die Verwendung von Scala in einer Version vor 2.8 macht es nötig, zwischen denFiltern ein Semikolon zu schreiben, selbst wenn wir die Filter auf zwei Zeilenverteilen. Sie können dies aber durch Verwendung von geschweiften Klammernumgehen.Sie können auch mehrere Generatoren angeben. Diese müssen in runden Klam-mern auch in 2.8 durch Semikolons getrennt werden. Daher verwenden wir imfolgenden Beispiel geschweifte Klammern:

for {x <- 1 to 10t <- 2 to x-1if (x%t==0)

} println(t+" teilt "+x)

Der obige Ausdruck verhält sich wie zwei verschachtelte for-Schleifen. Der zwei-te Generator entspricht dabei dem Kopf der inneren Schleife. Der Bezeichner xnimmt also nacheinander die Werte 1 bis 10 an. Für jeden Wert von x nimmt tdie Werte 2 bis x-1 an. Ist x-1 kleiner als 2, ist der berechnete Range leer. Istt Teiler von x, was durch den Filter angegeben wird, so wird t teilt x aus-

14 . . . und sich letztendlich wahrnehmbar auch so verhält.

Page 156: Scala : objektfunktionale Programmierung

5.6 For-Expressions 143

gegeben. Insgesamt erscheint also beim Auswerten der oben angegebenen for-Comprehension Folgendes auf der Konsole:

2 teilt 42 teilt 63 teilt 62 teilt 84 teilt 83 teilt 92 teilt 105 teilt 10

Neben der Angabe von Generatoren und Filtern können im ersten Teil des for-Ausdrucks Variablen definiert werden, die innerhalb des zweiten Teils gültig sind.Im folgenden Beispiel wird die Zeichenkette t teilt x der Variablen str zuge-wiesen, die dann für println genutzt werden kann. Eine so eingeführte Variableentspricht wieder einem val.

for {x <- 1 to 10t <- 2 to x-1if (x%t==0)str = t+" teilt "+x

} println(str)

Verwenden Sie runde Klammern, so muss auch zwischen dem Filter und der De-finition von str ein Semikolon stehen. Selbstverständlich kann als zweiter Teildes for-Ausdrucks ein Code-Block und nicht nur eine einzelne Zeile angegebenwerden, wie z.B. bei folgendem Ausdruck:

for {x <- 1 to 10t <- 2 to x-1if (x%t==0)

}{print(t)print(" teilt ")println(x)

}

Besonders interessant wird eine for-Comprehension durch das Schlüsselwortyield15, mit dem die Ergebnisse jedes Schrittes in eine neue Collection auf-gesammelt werden können. Das Schlüsselwort yield wird zwischen dem ers-ten und zweiten Teil des for-Ausdrucks angegeben. Mit der folgenden for-Comprehension wird ein Vector von Paaren, bestehend aus dem jeweiligen Tei-ler t und der Zahl x erzeugt:

15 Da yield ein Schlüsselwort in Scala, aber nicht in Java ist, muss die Methode yield in Scalabeispielsweise als Thread.‘yield‘ aufgerufen werden. Thread.yield führt zur Fehlermeldungerror: identifier expected but ’yield’ found.

Page 157: Scala : objektfunktionale Programmierung

144 5 Funktionales Programmieren

val pairs = for {x <- 1 to 10t <- 2 to x-1if (x%t==0)

} yield (t,x)

Ein anschließend ausgeführtes println(pairs) gibt

Vector((2,4), (2,6), (3,6), (2,8), (4,8), (3,9), (2,10),(5,10))

aus. Das gleiche Ergebnis könnten wir auch mit den Higher-Order-Functions map,filter und flatMap16 beispielsweise so programmieren:

(1 to 10) flatMap (x => (2 to x-1) map (t => (t,x)))filter {case (t,x) => x%t==0}

Tatsächlich wird eine for-Comprehension vom Compiler auch in einen solchenAusdruck transformiert, bevor dieser in Bytecode übersetzt wird. Bereits an demnoch recht einfachen Beispiel wird deutlich, dass die Schreibweise mit for min-destens für den ungeübten funktionalen Programmierer deutlich einfacher zu le-sen und zu verstehen ist.Eine for-Comprehension ist für alle Collections möglich, beispielsweise auch füreine Map, wie wir mit dem folgenden, etwas umfangreicheren Beispiel illustrierenwollen. Zunächst definieren wir in Listing 5.49 Case-Klassen für Vorlesungen undfür ein Studium.

Listing 5.49: Die Case-Klassen Course und Study

case class Course(name: String,passed: Boolean,creditPoints: Int)

case class Study(semester: Map[Int,List[Course]])

Eine Vorlesung hat einen Namen und eine Anzahl an Creditpoints. Außerdementhält unsere Vorlesungsklasse ein Flag, ob die Vorlesung bestanden wurde odernicht. Holgers bislang zweisemestriges Studium mit seinen bisherigen Leistungenist in Listing 5.50 als Beispiel angegeben.

Listing 5.50: Holgers bisheriges Studium

val holgersStudy = Study(Map(1 -> List(Course("Prog 1", true, 5),

Course("OS 1", false, 3),Course("Networks 1", true, 3)),

2 -> List(Course("Prog 2", true, 5),Course("OS 2", false, 3),Course("Networks 2", false, 3))))

16 Die Funktion flatMap wendet wie map eine Funktion auf jedes Element an.

Page 158: Scala : objektfunktionale Programmierung

5.6 For-Expressions 145

Nun können wir beispielsweise mit der in Listing 5.51 dargestellten for-Com-prehension die Liste der Vorlesungen berechnen, die Holger erfolgreich absolvierthat. Dazu verwenden wir im ersten Generator Pattern Matching, um aus den Paa-ren der Map die zweite Komponente zu extrahieren. Der zweite Generator läuftüber die Vorlesungen, und mit dem Filter überprüfen wir, ob das „Bestanden”-Flag gesetzt ist. Schließlich fügen wir den Namen der Vorlesung in die zu berech-nende Collection ein.

Listing 5.51: Berechnung der Namen der von Holger erfolgreich absolvierten Vorlesungen

val coursesPassed = for {(_,courses) <- (holgersStudy semester)c <- coursesif c.passed

} yield c.name

Der Wert von coursesPassed ist List(Prog 1, Networks 1, Prog 2).Im nächsten Schritt wollen wir statt der einfachen Liste der Vorlesungen ein Tu-pel, bestehend aus der Zahl der bisher insgesamt erreichten Creditpoints und derListe der Vorlesungen, berechnen. Der naheliegende Ansatz ist, zunächst die Lis-te der Paare, bestehend aus Punktzahl und Name mit einem for-Ausdruck, zuberechnen und mit einem foldRight die Punkte zu summieren und die Liste zuerzeugen, wie in Listing 5.52 gezeigt.

Listing 5.52: Berechnung von Paaren aus den Creditpoints und den Namen der von Holgererfolgreich absolvierten Vorlesungen – fehlerhaft

val coursesPassedWithPoints = for {(_,courses) <- (holgersStudy semester)c <- coursesif c.passedn = c.namep = c.creditPoints

} yield (p,n)

val creditPointsAndCoursesPassed =(coursesPassedWithPoints :\ (0,List[String]())) {case ((p,n),(sum,ns)) => (p+sum,n::ns)}

Interessanterweise hat creditPointsAndCoursesPassed den Wert:

(8, List(Prog 2, Networks 1)

Die bestandene Vorlesung Prog 1 ist im Ergebnis verschwunden. Sie taucht we-der in der Liste auf noch trägt sie zur Summe der Creditpoints bei. Aber wo liegtder Fehler? Auch bei näherer Betrachtung scheinen wir alles richtig gemacht zuhaben.

Page 159: Scala : objektfunktionale Programmierung

146 5 Funktionales Programmieren

Betrachten wir das Zwischenergebnis, so wird das Endergebnis klar, denn derWert von coursesPassedWithPoints ist Map((5,Prog 2), (3,Networks1)). Nachdem Prog 2 und Prog 1 beide 5 Creditpoints haben, taucht nur einerder beiden Werte mit dem „Schlüssel” 5 in der Map auf, und zwar der zuletztberechnete Wert.Nun bleibt nur noch die Frage zu klären, warum das Ergebnis von courses-PassedWithPoints eine Map und keine Liste von Tupeln ist. Dies liegt ganzeinfach daran, dass wir im ersten Generator eine Map verwenden und die for-Comprehension, wenn möglich, die gleiche Art Collection erzeugt. Im Listing 5.51haben wir nur einen einzelnen Wert nach dem yield angegeben. Damit konntedas Ergebnis keine Map werden. In Listing 5.52 ist es aber ein Tupel und kann alsSchlüssel-Wert-Paar interpretiert werden.Eine korrekte Lösung der Aufgabenstellung, ein Paar bestehend aus der Gesamt-zahl der Creditpoints und der Liste der bestandenen Vorlesungen zu bilden, ist inListing 5.53 dargestellt.

Listing 5.53: Berechnung von Paaren aus den Creditpoints und den Namen der von Holgererfolgreich absolvierten Vorlesungen – korrekt

val coursesPassedWithPoints = for {(_,courses) <- ((holgersStudy semester) toList)c <- coursesif c.passedn = c.namep = c.creditPoints

} yield (p,n)

val creditPointsAndCoursesPassed =(coursesPassedWithPoints :\ (0,List[String]())) {case ((p,n),(sum,ns)) => (p+sum,n::ns)}

Einziger Unterschied zum Code in Listing 5.52 ist die Umwandlung der Mapin eine Liste aus Tupeln vor Verwendung im ersten Generator mit der Metho-de toList. Damit hat creditPointsAndCoursesPassed, wie erwartet, denWert:

(13,List(Prog 1,Networks 1,Prog 2))

Der Wert von coursesPassedWithPoints ist nun tatsächlich List((5,Prog1),(3,Networks 1),(5,Prog 2)), also eine Liste von Tupeln und keine Mapmehr.

Page 160: Scala : objektfunktionale Programmierung

5.7 Typsystem 147

5.7 Typsystem

Scala ist statisch typisiert. Das heißt, Typüberprüfungen finden zum Überset-zungszeitpunkt und nicht erst zur Laufzeit statt wie bei dynamisch typisiertenSprachen. Dennoch gilt der typische Nachteil statischer Typisierung (also die Not-wendigkeit, überall Typen anzugeben) für Scala nicht. Scala hat einen Typinfe-renzmechanismus, mit dem der Typ eines Ausdrucks in den meisten Fällen au-tomatisch ermittelt werden kann. Die Stellen, an denen doch ein Typ angegebenwerden muss, haben wir bereits in Abschnitt 3.1 besprochen.Ohne an dieser Stelle in die zum Teil in einen Glaubenskrieg ausufernden Dis-kussionen über statische vs. dynamische Typisierung einsteigen zu wollen, bleibtdoch festzustellen, dass eine große Anzahl von Programmierfehlern Typfehlersind, die bei der statischen Typisierung durch den Programmierer, bei dynami-scher unter Umständen erst durch einen Nutzer entdeckt werden.

5.7.1 Standardtypen

Auch wenn die Scala-Standardbibliothek erst in Kapitel 6 vorgestellt wird, so wol-len wir an dieser Stelle doch schon einige Informationen über die Typen in Scalafesthalten. An der Spitze der Typhierarchie in Scala steht der Typ Any. Die beidenSubtypen von Any sind AnyVal und AnyRef.Der Typ AnyVal, der für die sogenannten primitiven Scala-Typen steht, hat flachnebeneinander die Subtypen Boolean, Byte, Short, Char, Int, Long, Float,Double und Unit.17

Der Typ AnyRef ist Basistyp aller anderen Typen, auch selbst definierter, und ent-spricht auf der Java-Plattform java.lang.Object, auf .NET System.Object.Zwischen AnyRef und den Scala-Referenztypen steht noch der Typ ScalaOb-ject, der als Marker dazu dient, Scala-Klassen von Java-Klassen zu unterschei-den.Außerdem werden in Scala noch sogenannte bottom types definiert. Der Typ Nullmit dem Wert null ist Subtyp von allen AnyRef-Subtypen. Der Typ Nothing istSubtyp von allen, also auch von primitiven Typen.Die Nichtexistenz eines gesuchten Wertes wird in Scala durch den DatentypOption zum Ausdruck gebracht. Betrachten wir beispielsweise die Suche nacheinem Wert zu einem Schlüssel in einer Map. Kann zu dem Schlüssel k keinSchlüssel-Wert-Paar gefunden werden, wird dies durch das Ergebnis None ange-zeigt. Wird ein Schlüssel-Wert-Paar (k,v) gefunden, muss der Wert v nun auchin den Option-Typ eingebettet werden. Daher wird in diesem Fall Some(v) zu-rückgegeben. Dadurch wird, anders als beispielsweise in Java, wo eine Nullrefe-

17 Die ersten acht primitiven Typen entsprechen den primitiven Typen in Java und werden auf der JVM(wenn möglich auch direkt darauf) abgebildet, obwohl es sich in Scala bei den Werten dieser Typenum Objekte handelt. Der Typ Unit entspricht dem Java-Typ void. Einziger Wert vom Typ Unit istdas Nulltupel ().

Page 161: Scala : objektfunktionale Programmierung

148 5 Funktionales Programmieren

renz zur Anzeige der Nichtexistenz genutzt wird, bereits im Typ einer Funktionsichtbar, dass ein Ergebnis optional ist.

5.7.2 Parametrischer Polymorphismus und Varianz

Scala unterstützt parametrische Datentypen. Das sind Datentypen, die mit einemTypparameter definiert werden. Diese Typen werden in Java beispielsweise ge-nerische Typen genannt. Bei einem konkreten Wert wird der Typparameter danndurch einen Typ instanziiert. Beispielsweise haben Listen den Typ List[T] füreinen Typ T. Eine konkrete Liste hat dann einen konkreten Typ, z.B.

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

Sowohl Typparameter als auch der konkrete Typ für den Parameter stehen in ecki-gen Klammern. Hat ein generischer Typ mehr als einen Typparameter, werdendiese durch Kommata getrennt, z.B. Map[A, B]. Ein parametrisierter Typ wiez.B. List oder Map wird auch als type constructor bezeichnet.Typkonstruktoren mit zwei Typparametern können auch infix geschrieben wer-den, d.h. statt Map[A,B] könnten wir auch schreiben A Map B. Interessant wirddiese Schreibweise im Zusammenhang mit Namen von Typen, die wie Opera-toren aussehen, z.B. +. Mit sogenannten Typsynonymen können wir bestehendenTypen neue Namen geben. Beispielsweise könnten wir ein Synonym für Map wiefolgt definieren:

type ->[A,B] = Map[A,B]

Mit diesem Typsynonym und der Infixschreibweise können wir dann den Typeiner Map[Int,String] schreiben als

Int -> String

An dieser Stelle ist der Hinweis sicher angebracht, dass Code verständlich und gutlesbar sein soll. Die Flexibilität von Scala lässt vieles zu. Ob dies in Ihren Projektenhilfreich ist oder eher verwirrend, müssen Sie in jedem Fall selbst entscheiden.Auch Funktionen können mit Typvariablen parametrisiert werden. Beispielsweiseerzeugt die folgende Funktion mkList aus einer beliebigen Anzahl von Elemen-ten vom Typ A eine Liste vom Typ List[A].

def mkList[A](xs: A*) = xs.toList

Wenden wir mkList auf verschiedene Elemente an, können wir uns in der Scala-Shell jeweils den konkreten Typ ansehen. Die folgende Sitzung soll dies verdeut-lichen:

scala> mkList()res0: List[Nothing] = List()

scala> mkList(1,2,3)

Page 162: Scala : objektfunktionale Programmierung

5.7 Typsystem 149

res1: List[Int] = List(1, 2, 3)

scala> mkList("One","Two","Three")res2: List[java.lang.String] = List(One, Two, Three)

scala> mkList(1,"Two",3.0)res3: List[Any] = List(1, Two, 3.0)

In der obigen Sitzung wird auch das sogenannte subtyping deutlich. Wird die leereListe ohne Informationen über den Typparameter erzeugt, wird der Bottom-TypNothing genommen. Werden wie im zweiten Fall drei Ints als Argumente derFunktion mkList übergeben, hat das Resultat den Typ List[Int]. Bei Stringsentsprechend List[String]. Haben die Argumente einen unterschiedlichenTyp, so wird der speziellste gemeinsame Supertyp genommen. Im Fall von ei-nem Int, einem String und einem Double ist der gemeinsame Supertyp Any.Damit bekommen wir in diesem Fall eine List[Any].Interessant ist beispielsweise auch folgender Fall

scala> mkList(1, 2.0)res4: List[Double] = List(1.0, 2.0)

In diesem Fall wird kein gemeinsamer Supertyp ermittelt, sondern durch eineimplizite Umwandlung aus dem Int ein Double gemacht. Wollen wir den Intwirklich als Int in der Liste behalten, können wir den Typparameter explizit mitAnyVal instanziieren:

scala> mkList[AnyVal](1, 2.0)res4: List[AnyVal] = List(1, 2.0)

In Scala verhalten sich generische Typen standardmäßig starr, d.h. nonvariant. ZurVeranschaulichung definieren wir eine Klasse MyClass mit einem Typparameter:

class MyClass[T]

Auch wenn ein Typ A Subtyp von einem Typ B ist, stehen MyClass[A] undMyClass[B] in keinerlei Subtypbeziehung zueinander. Definieren wir beispiels-weise die Funktion

def g(x: MyClass[AnyRef]) = x

so lässt sich diese ausschließlich auf einen Wert vom Typ MyClass[AnyRef] an-wenden, wie die folgende Sitzung veranschaulichen soll:

scala> val myAnyRef = new MyClass[AnyRef]myAnyRef: MyClass[AnyRef] = MyClass@40442b95

scala> val myString = new MyClass[String]myString: MyClass[String] = MyClass@4611dfe3

scala> g(myAnyRef)res11: MyClass[AnyRef] = MyClass@40442b95

Page 163: Scala : objektfunktionale Programmierung

150 5 Funktionales Programmieren

scala> g(myString)<console>:9: error: type mismatch;found : MyClass[String]required: MyClass[AnyRef]

g(myString)

Um den Datentyp flexibel, d.h. kovariant, zu machen wird dem formalen Typpara-meter ein + vorangestellt. Ändern wir also obige Definition class MyClass[T]zu

class MyClass[+T]

so gilt für jeden Subtyp A von einem Typ B auch: MyClass[A] ist Subtyp vonMyClass[B]. Konkret bedeutet es, dass wir g jetzt auch auf myString anwen-den können:

scala> class MyClass[+T]defined class MyClass

scala> def g(x: MyClass[AnyRef]) = xg: (x: MyClass[AnyRef])MyClass[AnyRef]scala> val myString = new MyClass[String]myString: MyClass[String] = MyClass@3432a325

scala> g(myString)res0: MyClass[AnyRef] = MyClass@3432a325

Der umgekehrte Fall heißt kontravariant und wird mit einem - annotiert. Definie-ren wir MyClass als

class MyClass[-T]

so können wir g zum Beispiel auf ein Objekt vom Typ MyClass[Any] anwenden,nicht jedoch auf MyClass[String]. Die so verwendeten Zeichen + und - heißenVarianz-Annotationen.Viele Scala-Collections sind kovariant, z.B. List, Seq oder Queue. Die Map istbeispielsweise für die Schlüssel nonvariant und für die Werte kovariant. Sie istdefiniert als Map[A, +B].Ein anschauliches Beispiel für die Verwendung von Kontravarianz sind dieFunction-Traits. Beispielsweise ist für einstellige Funktionen definiert

trait Function1[-T1, +R]

Das heißt also, Function1 ist kontravariant im ersten und kovariant im zweitenTypparameter. Definieren wir zu Veranschaulichung die folgende Klassen:

class Personclass Woman extends Person

und die Identitätsfunktion:

Page 164: Scala : objektfunktionale Programmierung

5.7 Typsystem 151

def h(p: Person) = p

so können wir als Argument statt einer Person auch einen Wert eines Subtypsvon Person verwenden, nicht aber einen Wert eines Supertyps. Umgekehrt kön-nen wir das Ergebnis als Wert eines Supertyps verwenden, nicht aber umgekehrt.Anders formuliert heißt das: Das Argument kann auch spezieller sein, und das Er-gebnis kann als Wert eines allgemeineren Typs verwendet werden. Die folgendeSitzung soll dies mit konkreten Beispielen verdeutlichen:

scala> h(new Woman)res0: Person = Woman@53601a4f

scala> h(new AnyRef)<console>:8: error: type mismatch;found : java.lang.Objectrequired: Person

h(new AnyRef)^

scala> h(new Woman): AnyRefres2: AnyRef = Woman@733638d4scala> h(new Woman): Woman<console>:9: error: type mismatch;found : Personrequired: Woman

h(new Woman): Woman^

Die Fehlermeldung von h(new AnyRef) bezieht sich auf das Argument. DieFehlermeldung bei h(new Woman): Woman auf das Ergebnis.Kovarianz und Kontravarianz sind nur für Klassen mit unveränderlichen Feldernzulässig. Betrachten wir die veränderliche Klasse

class MyMutableClass[T](var a: T)

Der Setter für das Feld a ist eine Funktion von T nach Unit. Dort dürfte T nonva-riant oder kontravariant sein. Der Getter gibt einen Wert vom Typ T zurück. Dortdürfte T nonvariant oder kovariant sein. Insgesamt bleibt daher nur die Möglich-keit der Nonvarianz übrig. Dies gilt immer dann, wenn auf ein Feld lesend undschreibend zugegriffen werden kann.

5.7.3 Upper und Lower Bounds

Die im vorherigen Abschnitt vorgestellten generischen Typen akzeptieren jedenbeliebigen Typparameter. Mit den Varianzannotationen ist es darüber hinausmöglich anzugeben, ob zwischen zwei konkreten parametrisierten Typen wie z.B.List[AnyRef] und List[String] eine Subtypbeziehung besteht. An man-chen Stellen ist es aber auch sinnvoll, von Beginn an die zulässigen Typparametereinzuschränken.

Page 165: Scala : objektfunktionale Programmierung

152 5 Funktionales Programmieren

Betrachten wir zur Verdeutlichung die in Listing 5.54 angegebenen KlassenPerson, Woman und Man.

Listing 5.54: Die Klassen Person, Woman und Man

abstract class Personclass Woman extends Personclass Man extends Person

Wir wollen weiter eine Klasse für eine Liste von Personen definieren. Ein ersterAnsatz wäre die folgende Definition:

class ListOfPersons(persons: Person*) {val list = persons.toListoverride def toString = list.toString

}

Damit können wir folgende Listen von Personen erzeugen:

scala> val listOfPersons =| new ListOfPersons(new Woman, new Man)

listOfPersons: ListOfPersons = List(Woman@2caee320,Man@dc160cb)

scala> val listOfWomen =| new ListOfPersons(new Woman, new Woman)

listOfWomen: ListOfPersons = List(Woman@409bad4f,Woman@2c8f3eac)

scala> val listOfMen=new ListOfPersons(new Man, new Man)listOfMen: ListOfPersons = List(Man@1a0283e, Man@39b1ff47

)

Schöner wäre es natürlich, wenn am Typ bereits zu erkennen wäre, ob es sich umeine Liste von Frauen, eine Liste von Männern oder eine gemischte Liste handelt.Dazu können wir die Klasse ListOfPersons so abändern, dass sie einen Typ-parameter hat, der dann mit Woman, Man oder Person instanziiert werden kann.Die Klasse würde dann wie folgt aussehen:

class ListOfPersons[T](persons: T*) {val list = persons.toListoverride def toString = list.toString

}

Wiederholen wir mit der parametrisierten Klasse unsere Eingaben von vorher,sehen wir, dass die Typisierung unseren Wünschen entspricht:

scala> val listOfPersons =| new ListOfPersons(new Woman, new Man)

listOfPersons: ListOfPersons[Person] = List(Woman@76eb2133, Man@46d0d843)

Page 166: Scala : objektfunktionale Programmierung

5.7 Typsystem 153

scala> val listOfWomen =| new ListOfPersons(new Woman, new Woman)

listOfWomen: ListOfPersons[Woman] = List(Woman@4c9549af,Woman@5d18a770)

scala> val listOfMen=new ListOfPersons(new Man, new Man)listOfMen: ListOfPersons[Man] = List(Man@448be1c9,

Man@3b947647)

Das Problem ist nun: Wir können auch Folgendes machen:

scala> val listOfInts=new ListOfPersons(1, 2, 3)listOfInts: ListOfPersons[Int] = List(1, 2, 3)

und das wollen wir ja eigentlich nicht. Die Lösung für unser Problem ist ein upperbound. Damit können wir die Menge der zulässigen Typparameter so einschrän-ken, dass es sich um Werte vom Typ Person oder davon abgeleitete handelt. EinUpper Bound wird direkt beim Typparameter mit dem Operator <: angegeben.Die Klasse ListOfPersons sieht dann wie in Listing 5.55 dargestellt aus.

Listing 5.55: Die Klasse ListOfPersons mit einem Upper Bound

class ListOfPersons[T <: Person](persons: T*) {val list = persons.toListoverride def toString = list.toString

}

Die Beispiele mit Frauen und Männern funktionieren so wie zuvor, aber der Ver-such, Ints zu verwenden schlägt jetzt mit einer Fehlermeldung fehl:

scala> val listOfInts=new ListOfPersons(1, 2, 3)<console>:6: error: inferred type arguments [Int] do not

conform to class ListOfPersons’s type parameter bounds[T <: Person]

val listOfInts=new ListOfPersons(1, 2, 3)^

Interessant sind Upper Bounds auch im Zusammenhang mit besonderen Eigen-schaften, die durch Traits ausgedrückt werden. Auch ein Trait kann in einem Up-per Bound angegeben werden. Definieren wir beispielsweise eine Funktion mitfolgender Signatur:

def sort[T <: Ordered[T]](xs: List[T]): List[T]

so können wir die Funktion sort für eine Liste von jedem Typ nutzen, der denTrait Ordered hineingemixt hat. Damit können die im Trait Ordered definiertenFunktionen zur Implementierung von sort genutzt werden.Analog zu den Upper Bounds gibt es in Scala auch Lower Bounds. Diese wer-den beispielsweise in der Klasse Queue der Standardbibliothek genutzt. Eine un-veränderliche Warteschlange ist kovariant, d.h. die Definition beginnt mit class

Page 167: Scala : objektfunktionale Programmierung

154 5 Funktionales Programmieren

Queue[+A]. Die Klasse bietet aber eine Methode enqueue die ein Element indie Warteschlange einreihen soll. Selbstverständlich bleibt die alte Warteschlan-ge unverändert und es wird eine neue zurückgegeben. Nichtsdestotrotz ist eineDefinition der Form

def enqueue(elem: A) = ... // error

nicht möglich, da das Argument an einer kontravarianten Position steht18. Hierhilft der Lower Bound. Statt ein Argument von Typ A erwartet enqueue ein Ar-gument von einem Typ B, für den gilt: B ist Supertyp von A, also kontravariant.Definiert wird der Lower Bound mit dem Operator >:. Also gilt für die enqueue-Methode:

def enqueue[B >: A](elem: B) = ...

Wir können auch gleichzeitig einen Lower und einen Upper Bound für einen Typangeben. Beispielsweise ist die Methode doit der in Listing 5.56 dargestelltenKlasse MyClass für alle x: C definiert, für die gilt C ist Supertyp von A und C istSubtyp von B.

Listing 5.56: Die Klasse MyClass mit der Methode doit, in der für den Parameter einLower und Upper Bound definiert ist

class MyClass[A,B] {def doit[C >: A <: B](x: C) = ...

}

5.7.4 Views und View Bounds

In Abschnitt 4.4 haben wir Implicits betrachtet. Diese Implicits werden an vielenStellen zur impliziten Konversion eines Typen in einen anderen genutzt. SolcheUmwandlungen werden auch views genannt. Das heißt also: Ein View vom Typ Szum Typ T ist ein impliziter Wert mit einem Funktionstyp S => T.Eine Funktion, die solch einen View nutzt, muss diesen als impliziten Parameterübergeben. Wollen wir beispielsweise eine Funktion schreiben, die das Maximumeiner Liste berechnet, so müssen für den Elementtyp die Methoden des Ordered-Traits verfügbar sein. Nutzen wir als Signatur die folgende, so ist die Funktionnur mit den Elementtypen nutzbar, die den Ordered-Trait implementieren:

def maxList[T <: Ordered[T]](elems: List[T]): T = ...

Nutzen wir einen View, so muss der Elementtyp den Ordered-Trait nicht imple-mentieren, sondern es muss eine implizite Konversion von T nach Ordered[T]verfügbar sein. Die Definition mit dem View als implizitem Parameter sieht dannfolgendermaßen aus:

18 Ein Funktionsparameter ist kontravariant – siehe Seite 150.

Page 168: Scala : objektfunktionale Programmierung

5.7 Typsystem 155

def maxList[T](elems: List[T])(implicit converter: T => Ordered[T]): T = ...

Im Rumpf der Funktion maxList muss der converter nicht explizit angewen-det werden. Er steht ja als implizite Umwandlung bereit. Daher ist der eingeführteName converter auch unnötig, und statt obiger Signatur kann kürzer geschrie-ben werden:

def maxList[T <% Ordered[T]](elems: List[T]):T = ...

Der Ausdruck T <% Ordered[T] ist ein sogenannter view bound und sagt aus:Die Funktion maxList kann auf eine Liste von Elementen eines beliebigenTyps T angewendet werden, solange T als Ordered[T] behandelt, d.h. in einOrdered[T] umgewandelt werden kann. Ist ein Typ T ohne Umwandlung be-reits ein Ordered[T], so gilt der View Bound selbstverständlich auch als erfüllt.

5.7.5 Context Bounds

Die Version 2.8 führt noch ein weiteres Konstrukt auf Typebene ein: den contextbound. Ein Context Bound für einen Typ T wäre zum Beispiel T: Ordered. ImGegensatz zum View Bound T <% Ordered[T] heißt das aber nicht, dass ei-ne Konversion von T nach Ordered[T] verfügbar sein muss, sondern nur, dasses einen Wert vom Typ Ordered[T] geben muss. Expandiert wird ein ContextBound der Form T: Ordered nämlich zu einem impliziten Parameter mit demTyp Ordered[T]. Ein Anwendungsbeispiel für die Context Bounds sind die Ma-nifeste, die im folgenden Abschnitt vorgestellt werden.

5.7.6 Arrays und @specialized

Arrays in Scala sind (k)eine Besonderheit. Arrays sollen in Scala keine beson-dere Behandlung erfahren und sich sauber in das Collection-Framework (sieheAbschnitt 6.2) einfügen, d.h. insbesondere die verschiedenen Funktionen höhererOrdnung wie z.B. map oder foldRight sollen auch auf Arrays anwendbar sein.Auf der anderen Seite sollen Scala-Arrays aber voll interoperabel mit Java-Arrayssein und vor allem auch die effiziente Java-Implementierung nutzen.Die Umsetzung von Arrays hat sich mit der Version 2.8 grundlegend geändert.Vor 2.8 wurden die Werte automatisch hin- und hergewandelt und ein wenig com-piler magic hinzugefügt. Dies allerdings führte zum Teil zu schlechter Performanzund zu anderen Problemen. Auf Arrays konnten die Collection-Methoden ange-wendet werden, allerdings war das Ergebnis dann kein Array mehr und damitdie Interoperabilität mit Java dahin.Das gleiche Problem bestand im Übrigen auch mit Strings. Ein Scala-String ent-spricht java.lang.String. Durch eine implizite Konversion in einen Rich-String konnte eine Vielzahl zusätzlicher Methoden mit einem String genutzt

Page 169: Scala : objektfunktionale Programmierung

156 5 Funktionales Programmieren

werden. Auch hier war das Ergebnis dann aber ein RichString, der nicht mehrkompatibel zum Java-String war.Die Lösung für Arrays und analog für Strings besteht nun darin, dass es statt ei-ner nun zwei implizite Umwandlungen gibt: in ein ArrayOps bzw. StringOpsund in ein WrappedArray bzw. WrappedString. Die Ops-Varianten definierendie Collection-Methoden, geben aber ein Array als Ergebnis zurück. Diese Um-wandlung von einem Array in ein ArrayOps ist damit so kurzlebig, dass sie vonmodernen virtuellen Maschinen sogar wegoptimiert werden kann. Die Wrapped-Varianten bleiben eine Scala-Collection und sind damit nicht mehr kompatibel zuden Java-Typen.Durch ein leicht modifiziertes Verfahren zur Ermittlung einer passenden im-pliziten Konversion können die Umwandlungen so priorisiert werden, dassan erster Stelle die Ops-Variante und an zweiter Stelle die Wrapped-Variantekommt. De facto ist es so, dass die Implicits für ArrayOps und StringOps indem Objekt Predef definiert sind und die Implicits für WrappedArray undWrappedString in der Klasse LowPriorityImplicits. Der Trick ist nun, dassLowPriorityImplicits die Basisklasse vom Objekt Predef ist. Mit dem mo-difizierten Verfahren haben damit die Implicits in Predef eine höhere Prioritätwie die geerbten von LowPriorityImplicits. Übrigens kann der Ansatz auchin eigenen Programmen genutzt werden um Implicits zu priorisieren.Ein weiteres (gelöstes) Problem ist das folgende: Scala hat generische Arrays, Javanicht. In Java gibt es neun verschiedene Repräsentationen eines Arrays: acht fürdie verschiedenen primitiven Datentypen19 und eine für Referenztypen.Scala macht diesen Unterschied nicht, sondern vereinheitlicht das Objektmodell.Aber wenn in einem Scala-Programm ein Array[Int] verwendet wird, soll die-ses natürlich durch das Java-int-Array repräsentiert werden. Um dies sicherzu-stellen, muss zur Laufzeit bekannt sein, welche Repräsentation zu wählen ist. Diegenerischen Typen in Scala basieren jedoch auf type erasure. Das heißt, die Typ-parameter werden zum Übersetzungszeitpunkt überprüft und nach erfolgreicherPrüfung entfernt. Also ist zur Laufzeit grundsätzlich keine Information über denTypparameter mehr vorhanden.Die Lösung dieses Problems heißt Manifest. Ein Wert vom Typ Manifest[T]bietet die vollständigen Informationen über den Typ T. Ein solches Manifestmuss in den meisten Fällen nicht einmal selbst erzeugt werden, sondern kannvom Compiler generiert werden und wird typischerweise als impliziter Para-meter übergeben. Das heißt, die einzige Stelle, an der uns üblicherweise dasManifest begegnet, ist die Signatur einer Funktion. Für Arrays reicht sogar dieabgeschwächte Version, das ClassManifest, welches schon generiert werdenkann, wenn die Top-Level-Klasse eines Typs bekannt ist. Bei Arrays muss ja nurfestgestellt werden, ob es ein Pendant eines primitiven Java-Typs oder irgendeinbeliebiger Referenztyp ist.

19 Primitive Datentypen in Java sind: byte, char, short, int, long, float, double und boolean.

Page 170: Scala : objektfunktionale Programmierung

5.7 Typsystem 157

Definieren wir nun zum Beispiel eine Funktion listToArray20, müssen wir dasClassManifest als impliziten Parameter definieren (siehe Listing 5.57).

Listing 5.57: listToArray mit einem impliziten ClassManifest-Parameter

def listToArray[T](list: List[T])(implicit m: ClassManifest[T]) = {

val xs = new Array[T](list.length)for (i <- 0 until list.length) xs(i) = list(i)xs

}

Nachdem der Parameter m in der gesamten Funktion listToArray überhauptnicht benötigt wird, kann er durch ein context bound anonymisiert werden, wie inListing 5.58 dargestellt ist.

Listing 5.58: Die Funktion listToArray mit einem Context Bound

def listToArray[T: ClassManifest](list: List[T]) = {val xs = new Array[T](list.length)for (i <- 0 until list.length) xs(i) = list(i)xs

}

Zu beachten ist insbesondere, dass auch Funktionen, die selbst gar kein Arrayerzeugen, aber eine Funktion aufrufen, die dies macht, ein ClassManifest be-nötigen. Definieren wir beispielsweise die Funktion:

def mkArray[T: ClassManifest](x: T*) =listToArray(x.toList)

so benötigt auch diese ein ClassManifest. Um ohne ClassManifest auszu-kommen, kann das GenericArray genutzt werden, das immer auf der Reprä-sentation als Referenz-Array basiert.Ein ganz ähnlich gelagertes Problem besteht grundsätzlich in der Verwendungvon primitiven Datentypen als Typparameter. Denn statt der direkten Verwen-dung des primitiven Datentyps auf der Java Virtual Machine wird dieser in einObjekt verpackt21. Dieses Verfahren wird boxing genannt. Programme, die solcheboxed primitive values verwenden, können bis zu zehnmal langsamer sein als sol-che, die direkt mit den primitiven Werten arbeiten. Dies führt dazu, dass vieleProgrammierer keine generischen Collections nutzen, sondern lieber spezialisier-te Versionen schreiben.

20 Die Funktion ListToArray dient nur als einfaches Beispiel, um das Konzept zu erklären. Selbst-verständlich kann eine Liste mit der Methode toArray direkt in ein Array umgewandelt werden.21 In Java können nur Referenztypen als Typparameter genutzt werden.

Page 171: Scala : objektfunktionale Programmierung

158 5 Funktionales Programmieren

Mit Scala 2.8 hält nun die @specialized-Annotation Einzug22. Damit könnenvom Compiler spezielle Versionen für die primitiven Typen generiert werden. Zielist es, auch in der Standardbibliothek spezialisierte Versionen generieren zu las-sen.Betrachten wir beispielsweise folgende Klassendefinition:

class Vector[@specialized A] {def apply(i: Int): A = // ...def map[@specialized(Int, Boolean) B]

(f: A => B) = // ...}

Aufgrund der @specialized-Annotation vor dem Typparameter A wird dieKlasse Vector für alle primitiven Typen spezialisiert. Die zweite Annotation@specialized(Int, Boolean) vor dem Typparameter B in der Definitionder Methode map führt dazu, das die Methode auch noch für Int und Booleanals Ergebnistyp der Funktion, die über den Vektor „gemappt” wird, spezialisiertwird.

5.7.7 Generalized Type Constraints

Scala bietet über die sogenannten generalized type constraints die Möglichkeit, Ty-pparameter einer Klasse oder eines Traits weiter einzuschränken. Die drei Cons-traints sind:

A =:= B heißt: Der Typ A muss B sein.

A <:< B heißt: A muss ein Subtyp von B (analog zu <:).

A <%< B heißt: A muss sich in B umwandeln lassen (analog zu <%).

Die drei Constraints entsprechen jeweils Klassen mit zwei Typparametern, also=:=[A,B], <:<[A,B] sowie <%<[A,B], werden aber üblicherweise wie obenangegeben in Infixnotation geschrieben.Die Constraints werden üblicherweise über einen impliziten Evidence-Parameterformuliert. In Listing 5.59 ist ein Beispiel dargestellt.

Listing 5.59: Die Klasse Foo mit Generalized Type Constraints

class Foo[A](a: A) {def stringLength(implicit evidence: A =:= String) =

a.lengthdef addIntToInt(x: Int)(implicit evidence: A <:< Int): Int = a + x

def addDoubleToNumber(x: Double)(implicit evidence: A <%< Double): Double = a + x

}

22 Da es noch Probleme im Zusammenspiel mit anderen Features gab, wurde die @specialized-Annotation noch nicht in 2.8.0 integriert.

Page 172: Scala : objektfunktionale Programmierung

5.7 Typsystem 159

Die Klasse Foo hat einen Typparameter A, der nicht eingeschränkt ist. In der Klas-se gibt es drei Methoden. Die Methode stringLength ist nur dann anwendbar,wenn der Typparameter der Typ String ist. Die Methode addIntToInt ist nurfür solche Instanzen von Foo nutzbar, bei denen es sich beim Typparameter umeinen Int handelt. Und schließlich muss sich der Typ für addDoubleToNumberin einen Double umwandeln lassen.Diese Methoden könnten zum Teil auch mit isInstanceOf und asInstanceOfformuliert werden. Der wesentliche Vorteil der generalisierten Typ-Constraintsist, dass sie zur Kompilierzeit und nicht erst zur Laufzeit überprüft werden. Diefolgende Sitzung soll das veranschaulichen:

scala> val string = new Foo("Hello World!")string: Foo[java.lang.String] = Foo@781fb069

scala> val int = new Foo(1234)int: Foo[Int] = Foo@2d791620

scala> val float = new Foo(1.23F)float: Foo[Float] = Foo@3479404a

scala> string stringLengthres0: Int = 12

scala> int stringLength<console>:8: error: could not find implicit value for

parameter evidence: =:=[Int,String]int stringLength

^

scala> float stringLength<console>:8: error: could not find implicit value for

parameter evidence: =:=[Float,String]float stringLength

^

scala> string addIntToInt 1<console>:8: error: could not find implicit value for

parameter evidence: <:<[java.lang.String,Int]string addIntToInt 1

^

scala> int addIntToInt 1res4: Int = 1235

scala> float addIntToInt 1<console>:8: error: could not find implicit value for

parameter evidence: <:<[Float,Int]float addIntToInt 1

^

scala> string addDoubleToNumber 4.56

Page 173: Scala : objektfunktionale Programmierung

160 5 Funktionales Programmieren

<console>:8: error: could not find implicit value forparameter evidence: <%<[java.lang.String,Double]

string addDoubleToNumber 4.56^

scala> int addDoubleToNumber 4.56res7: Double = 1238.56

scala> float addDoubleToNumber 4.56res8: Double = 5.790000019073486

5.7.8 Self-Type-Annotation

Mit einer self type annotation kann der Typ des Wertes this explizit deklariertwerden. Self-Type-Annotations können auch genutzt werden, um ein Synonymfür this zu definieren. Letzteres ist beispielsweise hilfreich, wenn aus einer in-neren Klasse auf eine Methode einer äußeren zugegriffen werden soll. In Listing5.60 ist ein Beispiel dafür angegeben. Eine Self-Type-Annotation besteht aus ei-nem Bezeichner, der unmittelbar nach der öffnenden Klammer einer Klasse odereines Traits geschrieben wird, und dem Doppelpfeil =>.

Listing 5.60: Verschachtelte Klassen A, B und C mit den Self-Type-Annotations selfA undselfB

class A { selfA =>def printIt(message: String) =

println("A.printIt: " + message)class B { selfB =>def printIt(message: String) =

selfA.printIt("B.printIt: " + message)class C {def printItA(message: String) =

selfA.printIt("C.printItA: " + message)def printItB(message: String) =

selfB.printIt("C.printItB: " + message)}val c = new C

}val b = new B

}

Direkt nach der öffnenden geschweiften Klammer der Klasse A wird als Synonymfür this der Bezeichner selfA definiert. Analog für Klasse B der BezeichnerselfB. In der Klasse B ist durch das eigene this das this aus Klasse A ver-schattet. Der Bezeichner selfA (oder ein beliebiger anderer) ist aber weiterhingültig und kann aus B heraus genutzt werden, um das Objekt vom Typ A, wel-ches das Objekt vom Typ B enthält, zu referenzieren. Analog verhält es sich für

Page 174: Scala : objektfunktionale Programmierung

5.7 Typsystem 161

die Klasse C. Dort sind selfA und selfB gültig. Die folgende Sitzung stellt diesam konkreten Beispiel dar:

scala> val a = new Aa: A = A@56092666

scala> a.printIt("Hello")A.printIt: Hello

scala> a.b.c.printItA("Hello")A.printIt: C.printItA: Hello

scala> a.b.c.printItB("Hello")A.printIt: B.printIt: C.printItB: Hello

Die explizite Deklaration des Typs von this ist sogar noch mächtiger. Betrachtenwir die drei Traits in Listing 5.61. Statt den Trait Animal durch trait Animalextends Noise with Food zu definieren, wird nur der Typ entsprechend an-gegeben. Dadurch sind genau wie bei einem extends ... die beiden MethodenmakeNoise und eat in Animal nutzbar. Die Version aus Listing 5.61 erscheintaber etwas natürlicher, denn ein Animal ist ja kein Noise und auch kein Food. Inanderen Programmiersprachen würde ein Animal vermutlich mit je einem Feldfür Noise und Food definiert werden.

Listing 5.61: Die Traits Noise, Food und Animal mit einer Self-Type-Annotation

trait Noise {def makeNoise: Unit

}

trait Food {def eat: Unit

}

trait Animal {self: Noise with Food =>

def run = {makeNoiseeatmakeNoise

}}

In Listing 5.62 ist die Implementierung der Klasse Dog dargestellt. Ein Dog ist einAnimal. Nachdem Animal aber über eine Self-Type-Annotation verfügt, mussDog noch eine passende Noise- und eine passende Food-Implementierung besit-zen. Dies wird über die Traits Bark und Gorge sichergestellt.

Page 175: Scala : objektfunktionale Programmierung

162 5 Funktionales Programmieren

Listing 5.62: Die Traits Bark und Gorge und die Klasse Dog

trait Bark extends Noise {def makeNoise = println("Woof, woof!")

}

trait Gorge extends Food {def eat = println("Gorge, gorge!")

}

class Dog extends Animal with Bark with Gorge

Die Implementierung der Klasse Bird mit den Traits Tweet und Pick ist in Lis-ting 5.63 dargestellt.

Listing 5.63: Die Traits Tweet und Pick und die Klasse Bird

trait Tweet extends Noise {def makeNoise = println("Chirp, chirp!")

}

trait Pick extends Food {def eat = println("Pick, pick!")

}

class Bird extends Animal with Tweet with Pick

Wir können anschließend einen Vogel und einen Hund erzeugen und beide „ab-laufen” lassen, wie die folgende Sitzung zeigt:

scala> val birdie = new Birdbirdie: Bird = Bird@36656758

scala> birdie.runChirp, chirp!Pick, pick!Chirp, chirp!

scala> val lassie = new Doglassie: Dog = Dog@3798f5e7

scala> lassie.runWoof, woof!Gorge, gorge!Woof, woof!

5.7.9 Strukturelle und existenzielle Typen

Mit einem strukturellen Typ (structural type) können wir angeben, dass wir einenTyp benötigen, der eine bestimmte Struktur hat. In Listing 5.64 ist ein Beispielangegeben.

Page 176: Scala : objektfunktionale Programmierung

5.7 Typsystem 163

Listing 5.64: Das Objekt RunWithTime und die Klasse RunMe

object RunWithTime {type Something = { def run() }def runWithTime(f: Something) = {import scala.compat.Platform.currentTimeval executionStart: Long = currentTimef.runval total = currentTime - executionStartprintln("[total " + total + "ms]")

}}

class RunMe {def run() {println("Starting")Thread.sleep(1000)println("Stopping")

}}

Im Objekt RunWithTime ist ein struktureller Typ Something angegeben. DieStruktur besagt, dass es ein beliebiger Typ sein kann, aber die Methode run fürden Typ definiert sein muss. Die Klasse RunMe definiert einen solchen Typ. Daherkann damit beispielsweise Folgendes ausgeführt werden:

scala> RunWithTime.runWithTime(new RunMe)StartingStopping[total 1005ms]

Dadurch ersparen wir uns letztendlich, ein Trait mit der abstrakten Methode runzu definieren, den die Klasse RunMe dann hinzufügt.Existenzielle Typen (existential types) können genutzt werden, um über Typen zuabstrahieren. Wird beispielsweise Pattern Matching mit einer Liste gemacht, istder Elementtyp zur Laufzeit aufgrund von Type Erasure nicht mehr vorhanden.Daher kann er als Unterstrich angegeben werden, also als List[_]. Die in Listing5.65 angegebene Funktion nutzt dies zum Pattern Matching mit Listen und Maps.

Listing 5.65: Die Funktion matchOnCollections, die existenzielle Typen zum PatternMatching für Listen und Maps nutzt

val matchOnCollections: AnyRef => Unit = {case _: List[_] => println("Eine Liste")case _: Map[_,_] => println("Eine Map")case _ => println("Etwas anderes")

}

Existenzielle Typen sind insbesondere dazu gedacht, Javas Wildcard und RawTypes aus Scala heraus zu nutzen. In Java gibt es beispielsweise die Typangabe

Page 177: Scala : objektfunktionale Programmierung

164 5 Funktionales Programmieren

Iterator<?>, was soviel heißt wie ein Iterator irgendeines Typs. Dies kannin Scala geschrieben werden als Iterator[_]. Genau genommen ist dies dieKurzschreibweise für:

Iterator[T] forSome { type T }

Analog dazu können wir zum Beispiel für den Java-Typ Iterator<? extendsComponent> in Scala schreiben:

Iterator[T] forSome { type T <: Component }

Auch dies kann mit der Platzhaltersyntax etwas kompakter geschrieben werdenals:

Iterator[_ <: Component]

Page 178: Scala : objektfunktionale Programmierung

Kapitel 6

Die Scala-Standardbibliothek

Die Scala-Standardbibliothek besteht aus dem Package scalamit einer Reihe vonModulen und Sub-Packages. In Abschnitt 6.1 geben wir Ihnen zunächst einenÜberblick und besprechen das Predef-Objekt, das automatisch für jedes Scala-Programm zur Verfügung steht. In Abschnitt 6.2 besprechen wir das Collection-Framework, das mit Scala 2.8 komplett überarbeitet wurde. Scala bietet einen ex-zellenten XML-Support. Wohlgeformte XML-Ausdrücke können direkt als Litera-le im Quellcode verwendet werden (siehe Abschnitt 6.3). Ein Ansatz, der auch ausder funktionalen Programmierung übernommen wurde, ist die Verwendung vonParser-Kombinatoren. Darauf gehen wir in Abschnitt 6.4 ein. Das Kapitel schließtmit einem kleinen Blick auf die Programmierung grafischer Benutzeroberflächenmit dem scala.swing-Package in Abschnitt 6.5.

6.1 Überblick und das Predef-Objekt

Das Package scala enthält eine Vielzahl von Typen und Werten. Typen sind Klas-sen, Traits oder Typsynonyme. Werte sind Objekte oder vals. Viele davon habenwir bereits behandelt oder werden wir noch behandeln. Die Typsynonyme undvals sind durch das Package-Objekt scala definiert, und zwar im Wesentlichen,um die mit der Version 2.8 verschobenen Klassen und Objekte1 zusätzlich unterihrem ursprünglichen Namen verfügbar zu machen.Wie in Abschnitt 5.7 bereits erwähnt, steht an der Spitze der Klassenhierarchie, diein Abbildung 6.1 dargestellt ist, die abstrakte Klasse Any. Die für Any und damitfür jedes beliebige Objekt definierten Methoden sind in Listing 6.1 dargestellt.

1 Mit der Version 2.8 wurde das Collection-Framework einem Redesign unterzogen (siehe Ab-schnitt 6.2). Beispielsweise war die Klasse scala.collection.immutable.List vorher unterscala.List zu finden. Durch ein entsprechendes Typsynonym kann auch mit 2.8 noch scala.Listverwendet werden.

Page 179: Scala : objektfunktionale Programmierung

166 6 Die Scala-Standardbibliothek

Any

AnyVal

Double

Float

Long

Int

Short

Byte

Unit

Boolean

Char

AnyRef

ScalaObject

List Option

. . . weitere Scala-Klassen

String

. . . weitereJava-Klassen

Nothing

Null

Abbildung 6.1: Die Scala-Klassenhierarchie

Listing 6.1: Die Methoden der Wurzel-Klasse Any

def equals(that: Any): Boolean

final def == (that: Any): Boolean =if (null eq this) null eq that else this equals that

final def != (that: Any): Boolean = !(this == that)

def hashCode: Int

def toString: String

def isInstanceOf[A]: Boolean

def asInstanceOf[A]: A

Page 180: Scala : objektfunktionale Programmierung

6.1 Überblick und das Predef-Objekt 167

In Scala werden Objekte anders als in Java üblicherweise mit == bzw. != vergli-chen. Die beiden Methoden sind mit dem final-Modifier markiert, sodass sienicht redefiniert werden können. Die Methode equals, auf die sich == und da-mit auch != abstützt, dient dazu, Gleichheit nach Bedarf zu redefinieren. Diessollte aber grundsätzlich mit einer passenden Redefinition von hashCode einher-gehen, da Objekte, die als gleich betrachtet werden, auch denselben Hash-Werthaben sollten, aber nur diese.Die Methode toString gibt die Darstellung des Objekts als String zurück undsollte auch in eigenen Klassen den Vorstellungen entsprechend redefiniert wer-den. Die beiden Methoden isInstanceOf und asInstanceOf ermöglichen zuüberprüfen, ob ein Objekt Instanz einer bestimmten Klasse ist, bzw. können zumCasten eines Objekts auf eine andere Klasse verwendet werden, z.B.

scala> "Hello".isInstanceOf[AnyRef]res0: Boolean = true

scala> "Hello".isInstanceOf[ScalaObject]res1: Boolean = false

scala> "Hello".asInstanceOf[AnyRef]res2: AnyRef = Hello

Für numerische Typen S und T mit x: S kann x.asInstanceOf[T] dazu ge-nutzt werden, um x in einen Wert vom Typ T umzuwandeln. Dies entspricht demAufruf der Methode x.toT, z.B.

scala> 1.7.asInstanceOf[Int]res3: Int = 1

scala> 1.7.toIntres4: Int = 1

Die Klasse Any hat zwei unmittelbare Subklassen: AnyVal und AnyRef. Die Klas-se AnyVal ist die Basisklasse für die Werte, die nicht als Objekte in Java imple-mentiert sind. Die neun Subklassen sind in Abbildung 6.1 dargestellt. Objekteder Klassen können nur durch die entsprechenden Literale, aber nicht mit newerzeugt werden. Die Klassen sind flach unter AnyVal angeordnet, die Werte kön-nen aber zum Teil implizit ineinander umgewandelt werden. Von AnyVal könnenkeine weiteren Klassen abgeleitet werden. Für AnyVal und alle Subtypen stehennatürlich die in Any definierten Methoden zur Verfügung.Die Klasse AnyRef ist Basisklasse für alle anderen Klassen: sowohl alle Scala-Klassen, die nicht Subklasse von AnyVal sind, alle Java-Klassen, die in Scala ge-nutzt werden, und auch alle selbst definierten Klassen. Darüber hinaus gibt esnoch den Trait ScalaObject, der automatisch alle Scala-Klassen, auch selbst de-finierte, im Gegensatz zu Java-Klassen als solche markiert. Das heißt, Sie kön-nen über isInstanceOf[ScalaObject] herausbekommen, ob es sich um eineScala-Klasse oder nicht handelt, z.B.

Page 181: Scala : objektfunktionale Programmierung

168 6 Die Scala-Standardbibliothek

scala> List(1,2,3).isInstanceOf[ScalaObject]res5: Boolean = true

scala> "Hello World".isInstanceOf[ScalaObject]res6: Boolean = false

Wie oben zu sehen, ist der String in Scala keine Scala-Klasse, sondern java.-lang.String und damit keine Instanz von ScalaObject. Die Klasse AnyRefentspricht auf der Java-Plattform übrigens java.lang.Object. Um Missver-ständnissen vorzubeugen: Wenn Sie eine Klasse in Scala definieren, müssen Sieweder AnyRef noch ScalaObject als Basisklasse bzw. Trait angeben2. Dies er-folgt automatisch.In der Klasse AnyRefwerden die abstrakten Methoden aus Any und zwei weitere,eq und ne, definiert. Die Methoden sind in Listing 6.2 aufgeführt.

Listing 6.2: Die Methoden der Klasse AnyRef

def equals(that: Any): Boolean = this eq that

final def eq(that: AnyRef): Boolean = ...

final def ne(that: AnyRef): Boolean = !(this eq that)

def hashCode: Int = ...

def toString: String = ...

Der Methodenaufruf this eq that gibt true genau dann zurück, wenn dasthis und that ein und dasselbe Objekt referenzieren3. Die Methoden neund equals werden durch eq definiert4. Die Implementierungen der Metho-den hashCode und toString entsprechen den Implementierungen in java.-lang.Object. Das heißt, der Hash-Wert wird aus der dem Objekt zugewiesenenSpeicheradresse berechnet, und toString gibt den Klassennamen, ein @-Zeichenund den Hash-Wert zurück. Für viele Klassen aus der Scala-Standardbibliotheksind die drei Methoden bereits sinnvoller definiert. Für eigene Klassen sollten Siedas auch tun.Es gibt einige Klassen, für die entweder eine spezielle Syntax (syntactic sugar) an-geboten wird oder für die der Compiler speziellen Code generiert. Die meistendavon haben wir schon kennengelernt:

die Klasse String mit den entsprechenden String-Literalen,

die Klassen Tuplen, für 1 ≤ n ≤ 22, die durch die Syntax (..,..,...)erzeugt werden können, und

2 Die Angabe von ScalaObject mit extends oder with bringt sogar einen Fehler: error: traitScalaObject is inherited twice.3 Damit entspricht this eq that immer dem Vergleich this == that in Java.4 Damit entspricht this == that, solange equals nicht redefiniert wurde, this == that in Java.

Page 182: Scala : objektfunktionale Programmierung

6.1 Überblick und das Predef-Objekt 169

die Klassen Functionn, für 1 ≤ n ≤ 22, die üblicherweise .. => .. =>... geschrieben werden.

Die Klasse Array ist eine Besonderheit für den Compiler, damit sie keine Beson-derheit für den Programmierer ist (siehe Abschnitt 5.7.6). Darüber hinaus gibt esnoch für die Klasse Node eine spezielle Syntax, die wir in Abschnitt 6.3 vorstellenwerden5.Eine Besonderheit in etwas anderer Hinsicht stellt das Predef-Objekt dar. DasObjekt und damit alles, was darin definiert ist, wird automatisch in jede Scala-Datei importiert. Im Folgenden wollen wir Ihnen einen groben Überblick überden Inhalt geben.Zunächst werden in PreDef einige Typsynonyme definiert, z.B.

type String = java.lang.Stringtype Class[T] = java.lang.Class[T]type Map[A, +B] = collection.immutable.Map[A, B]type Set[A] = collection.immutable.Set[A]

Außerdem ist alles, was im Package-Object scala definiert wurde, auch in Pre-def sichtbar. Weiter werden verschiedene Methoden für Fehler oder Zusicherun-gen (assertions) definiert, wie z.B. error, exit, assert, assume und require.Das heißt, wir können in unseren Programmen später einfach

assert(x>0)

schreiben, ohne dass dafür wie in Java ein Schlüsselwort reserviert wurde oder ei-ne spezielle Syntax notwendig ist. assert steht einfach nur für Predef.assertund ist eine ganz normale Methode. Implementiert wird assert dann beispiels-weise so:

def assert(assertion: Boolean) {if (!assertion)throw new java.lang.AssertionError("assertion failed")

}

Natürlich gibt es eine überladene Methode, die noch eine Message als Parame-ter hat. Als Nächstes wird in Predef das Typsynonym Pair für Tuple2 undTriple für Tuple3 mit den entsprechenden Companion-Objekten eingeführt.Ein bisschen mehr ausholen wollen wir bei der ArrowAssoc. Eine Map enthältSchlüssel-Wert-Paare. Das heißt, wir können eine Map beispielsweise wie folgt er-zeugen:

Map(Tuple2(1,"Eins"), Tuple2(2,"Zwei"))

Nachdem das Typsynonym Pair verfügbar ist, auch durch:

Map(Pair(1,"Eins"), Pair(2,"Zwei"))

5 Für die Ungeduldigen: Die Klasse Node repräsentiert XML-Knoten, und in Scala kann XML direktals Literal geschrieben werden.

Page 183: Scala : objektfunktionale Programmierung

170 6 Die Scala-Standardbibliothek

Nachdem es für Tupel eine spezielle Syntax gibt, auch durch:

Map((1,"Eins"), (2,"Zwei"))

Aber üblicherweise erzeugen wir in Scala eine Map mit folgender Syntax:

Map(1 -> "Eins", 2 -> "Zwei")

Dies ist keine spezielle Syntax, sondern hat mit der Klasse ArrowAssoc zu tun.Die Klasse ArrowAssoc definiert nämlich eine Methode mit dem Namen ->, dieaus dem Objekt, auf das die Methode angewendet wird, und dem Parameter einTupel macht. Das heißt, aus 1 -> "Eins" wird, wenn 1 eine Instanz der KlasseArrowAssoc wäre, das Paar (1,"Eins"). Und wie wird aus der 1 jetzt eineArrowAssoc? Durch die implizite Konversion:

implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] =new ArrowAssoc(x)

An diesem Beispiel können wir noch einmal sehr schön sehen, wie wir in Scalaneue Syntax einführen können. Weiter geht es im Objekt Predef mit Funktionenzur Ausgabe und zum Einlesen, z.B. println, print, readLine, readInt undvielen mehr. Diese Funktionen sind allesamt im Objekt Console definiert, unddurch das Vorhandensein in Predef können wir in unseren Programmen zumBeispiel einfach schreiben:

val x = readLine()print(x)

Predef enthält natürlich auch eine Vielzahl von impliziten Konversionen für dieprimitiven, numerischen Typen, für die verschiedenen Arrayrepräsentationen, fürAutoboxing der primitiven Typen in die entsprechenden Java-Referenztypen, z.B.Int nach java.lang.Integer und für Zeichenketten.Zu guter Letzt sind in Predef noch die in 5.7.6 besprochenen impliziten Kon-versionen mit geringerer Priorität aus der Basisklasse LowPriorityImplicitsverfügbar.

6.2 Das Collection-Framework

Mit der Version 2.8 wurde das Scala-Collection-Framework einem Redesign un-terzogen. Alle Collection-Klassen befinden sich nun im Package scala.collec-tion.generic enthält verschiedene Klassen und Traits, um Collections zu er-zeugen. Im Normalfall werden diese Klassen und Traits nicht unmittelbar vonuns verwendet und daher hier nicht weiter betrachtet.Von den meisten Collections gibt es drei Versionen: eine in scala.collection,eine im Sub-Package immutable und eine im Sub-Package mutable. Die Klas-se in scala.collection hat dasselbe Interface wie die entsprechende Klasse

Page 184: Scala : objektfunktionale Programmierung

6.2 Das Collection-Framework 171

in scala.collection.immutable, wobei Letztere garantiert unveränderbarsind. Die Collections im Sub-Package mutable haben zusätzliche Methoden, dieden Zustand der Collection verändern können.

Traversable

Iterable

Map

SortedMap

Set

SortedSet BitSet

Seq

Buffer Vector LinearSeq

Abbildung 6.2: Die wichtigsten Basisklassen des Collection-Frameworks

In Abbildung 6.2 sind die wichtigsten Basisklassen des Collection-Frameworksdargestellt. Alle Collection-Klassen haben Folgendes gemeinsam:

Eine Instanz kann durch Angabe des Klassennamens und der Elemente inKlammern erzeugt werden, z.B.6

Traversable(1, 2, 3)Map("B" -> "Berlin", "S" -> "Stuttgart")Set("Haskell", "Scala", "Java")List(1, 2, 3)HashMap(1 -> "one", 2 -> "two")

d.h. für jede Collection-Klasse ist die apply-Methode im Companion-Objektdefiniert.

Eine Collection wird als String auf dieselbe Weise repräsentiert, z.B.

scala> Set("Haskell", "Scala", "Java")res0: scala.collection.immutable.Set[java.lang.String]

= Set(Haskell, Scala, Java)scala> Map(1 -> "one", 2 -> "two")res1: scala.collection.immutable.Map[Int,java.lang.

String] = Map((1,one), (2,two))

Wie im letzten Listing zu sehen ist, wird als Default die unveränderliche Versioneiner Collection gewählt. Soll stattdessen die veränderliche Version genutzt wer-den, muss diese einfach importiert werden:

6 Die HashMap muss aus scala.collection.immutable oder scala.collection.mutableimportiert werden.

Page 185: Scala : objektfunktionale Programmierung

172 6 Die Scala-Standardbibliothek

scala> import scala.collection.mutable.Mapimport scala.collection.mutable.Map

scala> Map(1 -> "one", 2 -> "two")res2: scala.collection.mutable.Map[Int,java.lang.String]

= Map((2,two), (1,one))

Die Hierarchie der Basisklassen (siehe Abbildung 6.2) ist so entworfen, dass mög-lichst nichts an verschiedenen Stellen mehrfach definiert werden muss. In Scala2.7 war zum Beispiel in den meisten Collection ein eigenes map definiert. Dasheißt, konkret in der List gab es

def map[B](f: A => B): List[B]

und im Set

def map[B](f: A => B): Set[B]

nur weil einmal eine List[B] und das andere Mal ein Set[B] als Ergebnis her-auskam. Durch das kohärentere Design der Collections in Scala 2.8 wird map imWesentlichen nur einmal in Traversable definiert, nämlich als:

def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Traversable[A], B, That]):

That

Das heißt, es gibt einen impliziten Parameter, der dafür Sorge trägt, dass die rich-tige Collection als Ergebnis herauskommt. Damit dies funktioniert, muss jede Col-lection eine Methode newBuilder implementieren, die einen Builder für die Col-lection zurückgibt.Das heißt also, eine Vielzahl von Collection-Methoden sind bereits im Trait Tra-versable definiert. Die einzige abstrakte Methode von Traversable ist dieMethode

def foreach[U](f: Elem => U): Unit

die eine Funktion f mit jedem Element der Collection ausführt. Übrigens könnensolche traversierbaren Collections auch unendlich sein. Ein Beispiel ist die Klassescala.collection.immutable.Stream, die eine unendliche Liste repräsen-tiert, auf die lazy zugegriffen wird. Das bedeutet, dass das nächste Element derListe erst dann berechnet wird, wenn es tatsächlich benötigt wird. Unterschiedenwerden können endliche und unendliche Collections über das Ergebnis der Me-thode hasDefiniteSize.Der unmittelbare Subtrait von Traversable, Iterable, hat auch eine einzigeabstrakte Methode, nämlich

def iterator: Iterator[A]

Die Methode foreach wird mithilfe des Iterators implementiert. Trotzdemsollten Subklassen foreach aus Effizienzgründen implementieren. Weiter teilen

Page 186: Scala : objektfunktionale Programmierung

6.2 Das Collection-Framework 173

sich die Collections in Maps, Sets und Seqs auf, die wir im Folgenden einzelnvorstellen wollen.Hinter dem Trait Seq verbergen sich Sequenzen. Sequenzen haben einen Indexund können damit als partielle Abbildungen von Int zum Elementtyp verstan-den werden. Der Index beginnt bei 0 und endet bei der Anzahl der Elemente mi-nus Eins. Die apply-Methode der Sequenzen wird zum Zugriff auf die Elementeüber den Index genutzt, z.B.

scala> val list = List("Hallo","Welt")list: List[java.lang.String] = List(Hallo, Welt)

scala> list(1)res3: java.lang.String = Welt

Ist eine Seq veränderbar, kann sie über die update-Methode und den Index ver-ändert werden, z.B.7

scala> val linearseq = scala.collection.mutable.LinearSeq("Eins","Zwei","Drei")

linearseq: scala.collection.mutable.LinearSeq[java.lang.String] = MutableList(Eins, Zwei, Drei)

scala> linearseq(1) = "Zwo"scala> linearseqres4: scala.collection.mutable.LinearSeq[java.lang.String

] = MutableList(Eins, Zwo, Drei)

Die beiden Subtraits LinearSeq und Vector definieren keine zusätzlichen Me-thoden, sondern haben verschiedene Performanz-Charakteristika: Eine lineare Se-quenz hat effiziente head- und tail-Methoden. Ein Vektor hat effiziente apply-und length-Methoden. Veränderbare Vektoren haben außerdem eine effizienteupdate-Methode.Eine Besonderheit in der Collections-Bibliothek sind die Buffer, eine weitereSubklasse von Seq. Die Buffer gibt es ausschließlich in der veränderbaren Ver-sion. Sie bieten die Möglichkeit, Elemente zu ersetzen, einzufügen und zu ent-fernen. Neue Elemente werden effizient am Ende eines Buffers angefügt. Diewesentlichen zusätzlichen Methoden von Buffer sind

zum Einfügen:

buf insert (i, x) // insert element x at index ibuf insertAll (i, xs) // insert all at index i

zum Entfernen:

buf remove i // remove element at index ibuf remove (i, n) // remove n elems starting at ibuf -= x // remove element xbuf --= xs // remove all elements in xs

7 Zur Erinnerung: linearseq(1) = “Zwo” wird ersetzt durch linearseq.update(1,“Zwo”).

Page 187: Scala : objektfunktionale Programmierung

174 6 Die Scala-Standardbibliothek

zum Anhängen am Ende:

buf += x // append xbuf += (x, y, z) // append x, y, and zbuf ++= xs // append all in xs

zum Anfügen am Anfang:

x +=: buf // prepend xxs ++=: buf // prepend all in xs

Häufig genutzte Implementierungen von Buffer sind

scala.collection.mutable.ListBuffer

scala.collection.mutable.ArrayBuffer

Der Trait Set und seine Implementierungen repräsentieren Mengen. Das Wesent-liche mathematischer Mengen ist, dass kein Element mehr als einmal darin vor-kommen kann. Die apply-Methode ist so definiert, dass sie überprüft, ob das Ar-gument in der Menge enthalten ist, und entspricht damit der Methode contains.Das folgende Listing dient der Veranschaulichung:

scala> val set = Set(1,2,3)set: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> set(1)res5: Boolean = true // 1 ist in set enthalten

scala> set(4)res6: Boolean = false // 4 ist nicht in set enthalten

Elemente können je nachdem, ob das Set veränderlich ist oder nicht, mit + und++ oder += und ++= hinzugefügt und mit - oder -= entfernt werden, z.B.

scala> set + 4res7: scala.collection.immutable.Set[Int] = Set(1, 2, 3,

4)

scala> set - 1res8: scala.collection.immutable.Set[Int] = Set(2, 3)

scala> val mSet = scala.collection.mutable.Set(1,2,3)mSet: scala.collection.mutable.Set[Int] = Set(1, 2, 3)

scala> mSet += 4res9: mSet.type = Set(1, 4, 2, 3)

scala> mSet -= 1res10: mSet.type = Set(4, 2, 3)

Darüber hinaus sind in Set noch die üblichen Mengenoperationen definiert:

Page 188: Scala : objektfunktionale Programmierung

6.2 Das Collection-Framework 175

Schnittmenge:

xs & ysxs intersect ys

Vereinigungsmenge:

xs | ysxs union ys

Differenzmenge:

xs &~ ysxs diff ys

Teilmenge:

xs subsetof ys

leere Menge (nicht Test, ob leere Menge!):

xs.empty

Die beiden direkten Subtraits von Set sind SortedSet und BitSet. Ein Sor-tedSet ist eine Menge, die die Elemente mit foreach oder iterator in ei-ner frei wählbaren Reihenfolge zurückgibt. Ein BitSet ist eine Menge von nicht-negativen ganzen Zahlen, die wenig Speicher verbraucht, wenn der Bereich dermöglichen Werte klein ist. Der dritte unmittelbare Subtrait von Iterable ist Map.In einer Map werden Schlüssel-Wert-Paare gespeichert. Die Methode apply ist sodefiniert, dass sie als Argument einen Schlüssel bekommt und als Ergebnis dendazugehörigen Wert zurückgibt. Ist zum Schlüssel kein Eintrag vorhanden, wirdeine Exception geworfen, z.B.

scala> val map = Map("Eins" -> "One", "Zwei" -> "Two")map: scala.collection.immutable.Map[java.lang.String,java

.lang.String] = Map((Eins,One), (Zwei,Two))

scala> map("Zwei")res11: java.lang.String = Two

scala> map("Drei")java.util.NoSuchElementException: key not found: Drei

Die Methode get macht im Wesentlichen dasselbe, hat aber Option als Ergeb-nistyp. Das heißt, es wird entweder der Wert in ein Some verpackt oder statt derException None zurückgegeben, z.B.

scala> map.get("Zwei")res12: Option[java.lang.String] = Some(Two)

scala> map.get("Drei")res13: Option[java.lang.String] = None

Page 189: Scala : objektfunktionale Programmierung

176 6 Die Scala-Standardbibliothek

Eine Map hat zum Hinzufügen und Entfernen im Wesentlichen die gleichen Me-thoden wie Set: +, +=, - und -=. Updates sind mit verschiedener Syntax möglich:

für mutable Maps:

map(5) = "five"map.update(5, "five")map += (5 -> "five")

für immutable Maps:

map.updated(5, "five")map + (5 -> "five")

Die Methode update für unveränderliche Maps ist deprecated, d.h. zwar vor-handen, sollte aber nicht mehr genutzt werden. Der einzige direkte Subtrait istSortedMap, die nach den Schlüsseln sortiert ist.Gleichheit von Collections ist so definiert, dass die Collections zunächst in dreiKategorien eingeteilt werden: Sets, Maps und Seqs. Zwei Collections aus ver-schiedenen Kategorien sind immer ungleich. Zwei Collections aus derselben Ka-tegorie sind genau dann gleich, wenn sie dieselben Elemente enthalten. Es giltalso zum Beispiel:

scala> List(1,2,3) == Vector(1,2,3)res14: Boolean = true

scala> List(1,2,3) == Vector(1,3,2)res15: Boolean = false

scala> import| scala.collection.immutable.{HashSet, TreeSet}

import scala.collection.immutable.{HashSet, TreeSet}

scala> HashSet(1,2) == TreeSet(2,1)res16: Boolean = true

6.3 Scala und XML

Die Extensible Markup Language (XML)8 hat sich in vielen Bereichen durchge-setzt, z.B. der Rechner-zu-Rechner-Kommunikation über das Internet oder für dieKonfiguration von Applikationen. Scala bietet einen erstklassigen XML-Support:XML-Elemente können direkt als Literale geschrieben werden, z.B.

scala> <b>Hello World</b>res0: scala.xml.Elem = <b>Hello World</b>

8 XML ist vom World Wide Web Consortium (W3C) spezifiziert worden. Die aktuelle Version findenSie unter http://www.w3.org/TR/2008/REC-xml-20081126/. Wir werden an dieser Stelle nichtweiter auf XML eingehen. Siehe auch [HMUL05].

Page 190: Scala : objektfunktionale Programmierung

6.3 Scala und XML 177

Das oben angegebene XML-Literal entspricht einem Objekt der Klasse scala.-xml.Elem mit dem Label b und dem Nachfolger Hello World9. Die anderenwesentlichen Klassen des Packages scala.xml sind die folgenden:

Die abstrakte Klasse Node als Basisklasse für alle XML-Knoten.

Die Klasse NodeSeq, die eine Sequenz von Knoten repräsentiert. Ein einzelnerKnoten kann als Sequenz der Länge 1 betrachtet werden.

Die Klasse Text für einen Knoten, der nur Text enthält, z.B. Hello World.

In einem XML-Literal können beliebige Scala-Ausdrücke in geschweiften Klam-mern (sog. brace escapes) enthalten sein, z.B.

scala> <result>{5+7+9}</result>res1: scala.xml.Elem = <result>21</result>

scala> val list = (1 to 10).toListlist: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> <result>| Summe der Liste {list.toString} ist {list.sum}| </result>

res2: scala.xml.Elem =<result>Summe der Liste List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) ist

55</result>

XML-Literale und Brace Escapes können beliebig geschachtelt werden. Führenwir beispielsweise das Scala-Script aus Listing 6.3 aus, wird Folgendes ausgege-ben:

<author><me></me>

</author><author><name>Jon Spencer</name>

</author>

Listing 6.3: Scala-Script zur Veranschaulichung der Verschachtelung von XML und BraceEscapes

def author(name: String) =<author>{ if (name == "Oliver") <me />

else <name>{name}</name> }</author>println(author("Oliver"))println(author("Jon Spencer"))

9 Ein XML-Dokument wird als Baum repräsentiert.

Page 191: Scala : objektfunktionale Programmierung

178 6 Die Scala-Standardbibliothek

Durch die Verwendung von XML-Literalen werden übrigens die Zeichen <, >, &usw. in Strings automatisch in die entsprechende Escape-Sequenz umgewandelt.Dies hilft gegen das Problem, dass Benutzer beispielsweise in Feldern auf Web-seiten XML eingeben können und so ein potenzielles Sicherheitsloch öffnen. EinBeispiel zur Verdeutlichung ist das folgende:

scala> <a>{"</a>wieder raus & wieder rein <a>"}</a>res3: scala.xml.Elem = <a>&lt;/a&gt;wieder raus &amp;

wieder rein &lt;a&gt;</a>

Wenn Sie eine geschweifte Klammer in Ihrem XML haben möchten, müssen Siesie doppelt schreiben, z.B.

scala> <i>def f() {{ println("Hallo") }}</i>res4: scala.xml.Elem = <i>def f() { println(&quot;Hallo&

quot;) }</i>

Über einfache Methoden der Klasse NodeSeq können Teile extrahiert werden.Betrachten wir als Beispiel die in Listing 6.4 dargestellten XML-Werte scalabookund osgibook.

Listing 6.4: XML-Knoten für dieses Buch und das OSGi-Buch

val scalabook =<book lang="de"><title>Scala</title><author>Oliver Braun</author>

</book>

val osgibook =<book lang="de"><title>OSGi für Praktiker</title><authors><author>Bernd Weber</author><author>Patrick Baumgartner</author><author>Oliver Braun</author>

</authors></book>

Den Titel-Knoten des osgibooks können wir mit folgendem Ausdruck extrahie-ren:

scala> osgibook \ "title"res5: scala.xml.NodeSeq = NodeSeq(<title>OSGi</title>)

Die Autoren-Knoten können wir mit folgendem Ausdruck extrahieren:

scala> osgibook \\ "author"res6: scala.xml.NodeSeq = NodeSeq(<author>Bernd Weber</

author>, <author>Patrick Baumgartner</author>, <author>Oliver Braun</author>)

Page 192: Scala : objektfunktionale Programmierung

6.3 Scala und XML 179

Zugriff auf die verschiedenen Autoren funktioniert dann mit den für Sequenzenüblichen Methoden, z.B.

scala> (osgibook \\ "author")(2)res7: scala.xml.Node = <author>Oliver Braun</author>

scala> for (author <- osgibook \\ "author")| println("Autor: "+author.text)

Autor: Bernd WeberAutor: Patrick BaumgartnerAutor: Oliver Braun

Soll der Wert eines Attributs berechnet werden, muss dem Attribut ein @ voran-gestellt werden, z.B.

scala> osgibook \ "@lang"res8: scala.xml.NodeSeq = de

Auch Pattern Matching ist direkt mit XML-Literalen möglich. In Listing 6.5 ist dieFunktion countAuthors angegeben. Wenn es sich bei dem XML-Knoten um einBuch handelt, wird mit dem for-Ausdruck aus den Nachfolgern der authors-Knoten gesucht. Wenn gefunden, wird die Anzahl der enthaltenen author-Knoten zurückgegeben. Sollte kein authors-Knoten gefunden werden, gibt eswohl nur einen author-Knoten. Daher wird in diesem Fall 1 zurückgegeben.

Listing 6.5: Die Funktion countAuthors

def countAuthors(xml: scala.xml.Node): Int = {xml match {case <book>{ fields @ _* }</book> => {for (<authors>{authors @ _*}</authors> <- fields)return (authors \\ "author").length

return 1}

}}

Verwenden wir nun die beiden in Listing 6.4 definierten Bücher als Argument derFunktion countAuthors, bekommen wir folgende Ergebnisse:

scala> countAuthors(osgibook)res9: Int = 3

scala> countAuthors(scalabook)res10: Int = 1

Das Objekt scala.xml.XML definiert einige sehr nützliche Methoden im Um-gang mit XML, unter anderem die folgenden:

Mit der Methode load kann XML direkt aus einer Datei, einem Input-Stream, einer URL oder anderen Quellen gelesen werden.

Page 193: Scala : objektfunktionale Programmierung

180 6 Die Scala-Standardbibliothek

Mit der Methode loadFile kann eine XML-Datei in einen Node umgewan-delt werden.

Mit der Methode save kann ein XML-Knoten in eine Datei gespeichert wer-den.

Mit der Methode loadString kann aus einem String XML erzeugt werden.

6.4 Parser kombinieren

Sobald eine Scala-Applikation externe Daten auf irgendeinem Wege bekommt,müssen diese so aufbereitet werden, dass sie in der Applikation genutzt wer-den. Solche Daten können zum Beispiel Konfigurationsdateien, Daten, die überdas Netzwerk empfangen wurden, oder Eingabedaten in einem speziellen Formatsein. Eine Komponente oder ein Programm, das diese Zerlegung und Umwand-lung vornimmt, heißt Parser.Parser können aus einer Beschreibung der Daten mit sogenannten Parser-Gene-ratoren erzeugt werden. Bereits seit Mitte der 1980er Jahre hat sich aber in derFunctional Programming Community eine starke Tendenz entwickelt, stattdessen so-genannte Parser Kombinatoren10 zu nutzen. Während Parser-Generatoren externeTools wie Yacc, Bison oder ANTLR sind, werden Parser-Kombinatoren in der Pro-grammiersprache selbst verwendet. In der funktionalen Programmierung wirdeine Funktion, die aus Funktionen für eine Aufgabe wie z.B. das Parsen eine neueFunktion für dieselbe Art von Aufgabe kombiniert, Kombinator genannt.Die Vorteile solcher Parser-Kombinatoren liegen auf der Hand: Sie können einfachkomponiert werden, sind dadurch modularer, und es ist die gesamte Mächtigkeitder Programmiersprache für die Parser-Konstruktion verfügbar.Auch Scala hat eine Parser-Kombinator-Bibliothek, deren Nutzung wir im Folgen-den vorstellen wollen. Als Beispiel wollen wir folgende Eingabezeile parsen:

Mayr, Hubertus (123456): 90 Punkte = 1,0

Die Zeile ist ein Datensatz aus einer Datenbank für eine Prüfung. Der StudentHubertus Mayr mit der Matrikelnummer 123456 hat 90 Punkte und damit dieNote 1,0 erreicht. Wir wollen diese Daten in Scala weiter verwenden können. Dazusoll es eine Klasse Student geben, die in Listing 6.6 wiedergegeben ist.

Listing 6.6: Die Klasse Student

class Student(val name: String, val matNr: Int) {override def toString = name+" ("+matNr+")"

}

10 siehe auch [Hut92]

Page 194: Scala : objektfunktionale Programmierung

6.4 Parser kombinieren 181

Das Ergebnis einer Prüfung soll dann der Einfachheit halber nur als Tupel beste-hend aus dem Student, den Punkten und der Note repräsentiert werden, also fürHubertus Mayr:

val mayr = (new Student("Mayr, Hubertus",123456),90,1.0)

Zunächst beschränken wir uns auf die Aufgabe, die Zeichenkette überhaupt rich-tig zu parsen. Haben wir dies, ergänzen wir unsere Parser so, dass wir das Tupelals Ergebnis erhalten.Im Package scala.util.parsing.combinator gibt es eine Reihe von Parser-Traits, die wir für unsere Zwecke nutzen könnten. Beispielsweise hat der TraitJavaTokenParsers schon einige Parser, zum Beispiel für Strings und Gleit-punktzahlen. Der Trait JavaTokenParsers erweitert den Trait RegexParsers,den wir nutzen werden. Damit können aus regulären Ausdrücken Parser erzeugtwerden. Der Basis-Trait von RegexParsers ist Parsers. In Parsers sind diewesentlichen Parser-Kombinatoren definiert.Neu in Scala 2.8 sind die sogenannten PackratParsers, ein weiterer Subtraitvon Parsers. Ein Packrat-Parser ist ein spezieller Parser, der die Zwischener-gebnisse aller rekursiven Aufrufe behält und damit beispielsweise kontextfreieGrammatiken in linearer Zeit parst. Auf Packrat-Parser werden wir an dieser Stel-le nicht weiter eingehen.Ein Parser hat den Typ Parser[T], wobei T der Typ des Ergebnisses des Parsersist. Wir können beispielsweise einen Parser, der eine ganze Zahl parst11, folgen-dermaßen definieren:

val int: Parser[String] = """\d+""".r

Das heißt, wir geben einen regulären Ausdruck an, der auf eine ganze Zahl passt,denn \d steht für eine Ziffer, und das + heißt eines oder mehrere, also insgesamteine oder mehrere Ziffern. Damit der Backslash nicht als Steuerungszeichen in derZeichenkette interpretiert wird, haben wir drei Anführungstriche genommen. DieMethode r macht aus dem String einen regulären Ausdruck. Mehr müssen wirnicht tun. Ein regulärer Ausdruck ist ein Parser.Um den int-Parser nun tatsächlich so definieren zu können, müssen wir den TraitRegexParsers erweitern, wie in Listing 6.7 angegeben.

Listing 6.7: Die Klasse MyParsers

import scala.util.parsing.combinator._class MyParsers extends RegexParsers {val int: Parser[String] = """\d+""".r

}

11 Zur Erinnerung: Wir kümmern uns erst etwas später um den richtigen Ergebnistyp.

Page 195: Scala : objektfunktionale Programmierung

182 6 Die Scala-Standardbibliothek

Um die Zeichenkette

Mayr, Hubertus (123456): 90 Punkte = 1,0

parsen zu können, teilen wir sie erst einmal in die verschiedenen Komponentenauf und definieren für jede einen eigenen Parser. Natürlich machen wir uns dieLösung etwas einfacher, indem wir den Kontext mit einbeziehen.

Der Name ist alles, was vor der öffnenden runden Klammer steht12. Der re-guläre Ausdruck dazu ist ganz einfach [^(]* und bedeutet: Lese ein odermehrere Zeichen, die keine öffnende runde Klammer sind. Der Parser siehtdann so aus:

val studentname: Parser[String] = """[^(]+""".r

Die Matrikelnummer ist die ganze Zahl in runden Klammern. Hierzu könnenwir unseren int-Parser wiederverwenden und zwei Parser für die Klammerndazu kombinieren. Ein einfacher String ist übrigens auch ein Parser, d.h. "("ist der Parser für ( und ")" für ). Obwohl die Zeichen einfach nur nebenein-ander geschrieben sind, müssen wir dies hier explizit mit einem Kombinatorschreiben. Es gibt drei Kombinatoren für diese Art von Verknüpfung:

– a ~ b erzeugt einen neuen Parser, der zuerst mit a, dann mit b parst unddas Gesamtergebnis zurückgibt.

– a <~ b parst genauso wie a ~ b, gibt aber nur das Ergebnis des Parsers azurück.

– a ~> b gibt analog nur das Ergebnis von b zurück.

Nachdem wir die Klammern nicht im Ergebnis benötigen, sieht unser Parserso aus:

val matNr: Parser[String] = "("~>int<~"): "

Wir haben an dieser Stelle gleich den Doppelpunkt und das Leerzeichen vorden Punkten mitgenommen. Das hat den Vorteil, dass wir nicht zu viele einzel-ne kleine Parser definieren müssen, und den Nachteil, dass der matNr-Parseran einer anderen Stelle nicht unbedingt wiederverwendet werden kann.

Die Punkte sind wieder eine ganze Zahl, gefolgt von der Zeichenkette Punkte= . Zwischen den Punkten, der Zeichenkette Punkte und dem = können Leer-zeichen stehen. Leerzeichen werden in einem regulären Ausdruck als \W ge-schrieben. Wir benötigen als Ergebnis des Parsers nur den Punktwert, daherverwenden wir den Kombinator <~ in unserem Parser, der so aussieht:

val points: Parser[String] =int<~"""\W*Punkte?\W*=\W*""".r

12 Um das Leerzeichen am Ende des Namens kümmern wir uns später.

Page 196: Scala : objektfunktionale Programmierung

6.4 Parser kombinieren 183

Wir haben das e in Punkte mit einem Fragezeichen versehen, d.h. es kanneinmal oder keinmal vorkommen. Damit passt der reguläre Ausdruck auchauf 1 Punkt =.

Der letzten Teil-Parser, den wir benötigen, soll die Note parsen. Dazu müssenwir wissen, dass nach dem Komma nur die Werte 0, 3 und 7, für die 4 vordem Komma nur die 0 und die 3 und für die 5 sogar nur die 0 zulässig sind.Dies ist im regulären Ausdruck entsprechend berücksichtigt. Der Parser fürdie Note lautet also

val mark: Parser[String] ="""[123],[037]|4,[03]|5,0""".r

Was uns nun noch fehlt, ist der Parser für die gesamte Zeile. So wie wir die einzel-nen Parser definiert haben, müssen wir sie für den Parser nur mit ~ aneinanderhängen:

val oneExam: Parser[Any] = studentname~matNr~points~mark

Der oneExam-Parser gibt die vier Ergebnisse der Teil-Parser als kombiniertes Er-gebnis zurück. Daher geben wir hier als Ergebnistyp einfach Any an. Später wer-den wir den Wert entsprechend umwandeln.Um die Parser nun zum Parsen der oben angegebenen Zeile nutzen zu kön-nen, müssen wir sie nun noch in einer Klasse zusammenfassen. Die KlasseExamParserString ist in Listing 6.8 dargestellt.

Listing 6.8: Die Klasse ExamParserString

import scala.util.parsing.combinator._

class ExamParserString extends RegexParsers {

val int: Parser[String] = """\d+""".r

val studentname: Parser[String] = """[^(]+""".rval matNr: Parser[String] = "("~>int<~"): "val points: Parser[String] =

int<~"""\W*Punkte?\W*=\W*""".rval mark: Parser[String] =

"""[123],[037]|4,[03]|5,0""".r

val oneExam: Parser[Any] =studentname~matNr~points~mark

}

Zum Ausprobieren implementieren wir noch das Objekt ExamParserApp (sieheListing 6.9).

Page 197: Scala : objektfunktionale Programmierung

184 6 Die Scala-Standardbibliothek

Listing 6.9: Das Objekt ExamParserApp zum Parsen eines Datensatzes

object ExamParserApp extends ExamParserString {def main(args: Array[String]) {val mayrString ="Mayr, Hubertus (123456): 90 Punkte = 1,0"

println(parseAll(oneExam, mayrString))}

}

Die Funktion parseAll bekommt einen Parser, in unserem Fall oneExam, undden zu parsenden String, hier mayrString, als Argumente und wendet denParser auf die Zeichenkette an. Übersetzen wir die Klassen und führen das ObjektExamParserApp aus, so sehen wir Folgendes auf der Konsole:

[1.41] parsed: (((Mayr, Hubertus~123456)~90)~1.0)

Diese Ausgabe zeigt uns an, dass Zeile 1 bis Spalte 41 (also alles) erfolgreich ge-parst wurde. Das Ergebnis ist dahinter dargestellt. Bevor wir das Ergebnis in dasgewünschte Tupel umwandeln, wollen wir unseren Parser noch so erweitern, dasswir nicht nur einen Datensatz, sondern mehrere parsen können. Der dazu not-wendige Parser-Kombinator wiederholt die Anwendung des übergebenen Par-sers, solange es geht. Der Kombinator für die wiederholte Anwendung heißt rep.Nachdem wir die Datensätze über mehrere Zeilen verteilen wollen, lassen wirzwischen den Datensätzen Whitespaces zu. Daher verwenden wir den Kombina-tor repsep, der einen Parser für die Zeile und einen für den Separator als Para-meter hat. Der Parser für mehrere Datensätze lautet also

val exam: Parser[Any] = repsep(oneExam,"""\W*""".r)

Das Objekt ExamParserApp können wir zum Ausprobieren noch abändern – sie-he Listing 6.10.

Listing 6.10: Das Objekt ExamParserApp zum Parsen mehrerer Datensätze

object ExamParserApp extends ExamParserString {def main(args: Array[String]) {val examString ="""Mayr, Hubertus (123456): 90 Punkte = 1,0|Maler, Brigitte (876543): 86 Punkte = 1,3|Reimann, Gundolf (471112): 1 Punkt = 5,0""".

stripMarginprintln(parseAll(exam,examString))

}}

Führen wir das neue Objekt ExamParserApp aus, lautet die Ausgabe13:

13 Zur besseren Lesbarkeit wurden die Zeilen umbrochen und eingerückt.

Page 198: Scala : objektfunktionale Programmierung

6.4 Parser kombinieren 185

[3.41] parsed: List((((Mayr, Hubertus~123456)~90)~1.0),(((Maler, Brigitte~876543)~86)~1.3),(((Reimann, Gundolf~471112)~1)~5.0))

Das heißt, diesmal wurde bis Zeile 3 und Spalte 41 geparst und die Ergebnissealler Zeilen in eine Liste eingefügt.Nachdem wir in der Lage sind, die Informationen zu parsen, müssen wir nunnoch die Ergebnisse in die gewünschte Form bringen. Dazu sehen wir uns zu-nächst noch einmal das Ergebnis des Parsens eines Datensatzes an und verglei-chen es mit dem Parser. Das Ergebnis war:

(((Mayr, Hubertus~123456)~90)~1.0)

und der Parser war folgendermaßen aufgebaut:

studentname~matNr~points~mark

Fügen wir noch Klammern ein, so ist die Struktur des Parsers genau wie die desErgebnisses:

(((studentname~matNr)~points)~mark)

Und doch gibt es einen Unterschied. Die Tilde im Parser ist eine Methode unddie Tilde im Ergebnis der Name einer Case-Klasse, die infix ausgegeben wird undgeschrieben werden kann. Das heißt, wir können zum Transformieren des Ergeb-nisses Pattern Matching nutzen, um auf die verschiedenen Teilergebnisse zuzu-greifen.Zunächst müssen aber auch die Parser für die Teilergebnisse so umgebaut wer-den, dass sie den jeweils benötigten Wert zurückliefern. Für die beiden ParsermatNr und points ist das ein Int. Nachdem beide aber zum Parsen der Zahlauf den int-Parser zurückgreifen, müssen wir nur diesen anpassen. Zum Trans-formieren des Ergebnisses gibt es den ^^-Operator. Der erste Operand ist der Par-ser, der zweite eine Funktion, die das Ergebnis transformiert. Nachdem wir dieUmwandlung eines Strings in einen Int einfach mit toInt machen können,sieht der int-Parser jetzt so aus:

val int: Parser[Int] = """\d+""".r ^^ (_.toInt)

Für den mark-Parser, der die Note als Float zurückgeben soll, müssen wir erstdas Dezimalkomma durch einen Punkt ersetzen. Anschließend können wir denString mit toFloat in einen Float konvertieren. Der mark-Parser lautetalso:

val mark: Parser[Float] ="""[123],[037]|4,[03]|5,0""".r ^^(_.replace(’,’, ’.’).toFloat)

Obwohl der Studentenname ein String ist und daher das Ergebnis des stu-dentname-Parsers nicht in einen anderen Typ transformiert werden muss, nut-

Page 199: Scala : objektfunktionale Programmierung

186 6 Die Scala-Standardbibliothek

zen wir auch dort den ^^-Operator. Und zwar in diesem Fall, um mit der Metho-de trim das Leerzeichen, das in der ursprünglichen Zeichenkette zwischen demNamen und der runden Klammer stand, zu entfernen. Der neue studentname-Parser lautet damit:

val studentname: Parser[String] ="""[^(]+""".r ^^ (_.trim)

Jetzt können wir uns dem oneExam-Parser widmen, der nun wie folgt implemen-tiert wird:

val oneExam: Parser[(Student,Int,Float)] =studentname~matNr~points~mark ^^ {case name~nr~points~mark=> (new Student(name,nr), points, mark)

}

Mit Pattern Matching zerlegen wir das Ergebnis in seine vier Bestandteile. An-schließend setzen wir diese einfach zu dem benötigten Tripel zusammen. Dar-über hinaus haben wir auch den Typ des Parsers zu Parser[(Student, Int,Float)] geändert. Mit dem vorherigen Typ Parser[Any] wäre es zwar nachwie vor richtig, aber es ist immer sinnvoll, den spezielleren Typ anzugeben.Beim exam-Parser ändern wir nur den Typ. Die Implementierung kann unverän-dert bleiben. Die neuen Parser fassen wir in der Klasse ExamParser zusammen(siehe Listing 6.11).

Listing 6.11: Die Klasse ExamParser

import scala.util.parsing.combinator._class ExamParser extends RegexParsers {

val int: Parser[Int] = """\d+""".r ^^ (_.toInt)val studentname: Parser[String] = """[^(]+""".r ^^(_.trim)

val matNr: Parser[Int] = "("~>int<~"): "val points: Parser[Int] =int<~"""\W*Punkte?\W*=\W*""".r

val mark: Parser[Float] ="""[123],[037]|4,[03]|5,0""".r ^^(_.replace(’,’, ’.’).toFloat)

val oneExam: Parser[(Student,Int,Float)] =studentname~matNr~points~mark ^^ {case name~nr~points~mark=> (new Student(name,nr), points, mark)

}

val exam: Parser[List[(Student,Int,Float)]] =repsep(oneExam,"""\W*""".r)

}

Page 200: Scala : objektfunktionale Programmierung

6.5 Ein kleines bisschen GUI 187

Das Objekt ExamParserApp aus Listing 6.10 ändern wir nun so ab, dass es stattExamParserString die Klasse ExamParser erweitert (siehe Listing 6.12). DerRest bleibt unverändert.

Listing 6.12: Das Objekt ExamParserApp zum Parsen mehrerer Datensätze und einer Listevon Tripeln als Ergebnis

object ExamParserApp extends ExamParser {def main(args: Array[String]) {val examString ="""Mayr, Hubertus (123456): 90 Punkte = 1,0|Maler, Brigitte (876543): 86 Punkte = 1,3|Reimann, Gundolf (471112): 1 Punkt = 5,0""".

stripMarginprintln(parseAll(exam,examString))

}}

Führen wir das Objekt nun aus, bekommen wir als Ausgabe:

[3.41] parsed: List((Mayr, Hubertus (123456),90,1.0), (Maler, Brigitte (876543),86,1.3), (Reimann, Gundolf(471112),1,5.0))

Um das tatsächliche Ergebnis aus dem ParseResult-Objekt zu extrahieren, kön-nen wir die Methode get nutzen. Das heißt, zum Weiterarbeiten mit der Listewürden wir beispielsweise die beiden Zeilen

val resultList: List[(Student,Int,Float)] =parseAll(exam,examString).get

in unser Programm einfügen.Neben den behandelten Parser-Kombinatoren gibt es noch zwei weitere:

P | Q ist die Alternative. Das heißt, wenn der Parser P erfolgreich war, lieferter das Ergebnis, sonst der Parser Q.

opt(P) ist ein Options-Parser. Ist der Parser P erfolgreich und hat als Ergeb-nis x, liefert opt(P) den Wert Some(x) zurück. Andernfalls ist das ErgebnisNone. Damit bricht der Parsing-Vorgang nicht ab.

6.5 Ein kleines bisschen GUI

Die Standard-Scala-Distribution kommt zwar mit keiner eigenen GUI14-Bibli-othek, aber mit einem Package voller Wrapper für die Java-Swing-Klassen imPackage scala.swing. Ziel der Entwickler ist es, eine Bibliothek zu bieten, diesich in Scala auf sehr natürliche Art und Weise nutzen lässt und nah genug an Java

14 Graphical User Interface

Page 201: Scala : objektfunktionale Programmierung

188 6 Die Scala-Standardbibliothek

Swing bleibt, sodass Java-Programmierer mit Swing-Erfahrung gut damit zurechtkommen.Um ein einfaches GUI zu erzeugen, soll zunächst die abstrakte Klasse Simple-SwingApplication als Basisklasse genutzt werden. Die einzige abstrakte Me-thode, die definiert werden muss, ist die Methode:

def top: Frame

Frames für das Hauptfenster werden durch die Klasse MainFrame repräsentiert.Das einfachste GUI kann durch das in Listing 6.13 dargestellte Programm erzeugtwerden.

Listing 6.13: Ein leeres Window ohne Titel

import scala.swing._

object MyGUI extends SimpleSwingApplication {def top = new MainFrame

}

Allerdings hat dieses GUI weder einen Titel noch irgendeinen Inhalt. Dies könnenwir ändern, indem wir beispielsweise dem Feld title einen String zuweisen.In Listing 6.14 ist der Code für ein leeres Window mit dem Titel Scala-BuchGUI wiedergegeben. Das erzeugte Window ist in Abbildung 6.3 zu sehen.

Listing 6.14: Ein leeres Window mit Titel

import scala.swing._

object MyGUI extends SimpleSwingApplication {def top = new MainFrame {title = "Scala-Buch GUI"

}}

Abbildung 6.3: Ein leeres GUI-Window mit einem Titel

Das in Abbildung 6.3 dargestellte Window musste allerdings erst „von Hand”groß gezogen werden, da es sich standardmäßig als sehr kleines Fenster in derlinken oberen Ecke des Bildschirms öffnet. Dies ist aber auch einfach zu ändern.Das in Listing 6.15 definierte Objekt öffnet ein Fenster der Größe 640x480, wobei

Page 202: Scala : objektfunktionale Programmierung

6.5 Ein kleines bisschen GUI 189

die linke obere Ecke die Koordinate (200,300) hat. Die Zahlen verstehen sichwie üblich als Pixel.

Listing 6.15: Ein leeres Window mit Titel, einer minimalen Größe und einer Lokation

import scala.swing._

object MyGUI extends SimpleSwingApplication {def top = new MainFrame {title = "Scala-Buch GUI"location=new Point(200,300)minimumSize=new Dimension(640, 480)

}}

Menüs und Menüeinträge werden dem Feld menuBar zugewiesen. Dazu benö-tigen wir zunächst eine MenuBar. Dem Buffer content können wir Kompo-nenten hinzufügen. Im Code in Listing 6.16 sind dies Menus. Diese haben wieder-um auch einen Buffer content, dem wir dann MenuItems hinzufügen können.An die MenuItems schließlich hängen wir Actions,die ausgeführt werden, wenndas Menüelement angeklickt wurde.

Listing 6.16: Ein leeres Window mit Menüeinträgen und Aktionen zum Beenden und zumÖffnen eines Dialogfensters

import scala.swing._

object MyGUI extends SimpleSwingApplication {def top = new MainFrame {title = "Scala-Buch GUI"location=new Point(200,300)minimumSize=new Dimension(640, 480)val quitAction = Action("Quit") {System.exit(0)}val sayHelloAction = Action("Say Hello") {Dialog.showMessage(new FlowPanel,"Hello World!")

}menuBar = new MenuBar {contents += new Menu("File") {contents += new MenuItem(quitAction)

}contents += new Menu("Misc") {contents += new MenuItem(sayHelloAction)

}}

}}

In Listing 6.16 haben wir ein GUI definiert, das zwei Menüs hat. Das File-Menühat den Eintrag Quit, der das Programm beendet. Das Misc-Menü hat den Ein-

Page 203: Scala : objektfunktionale Programmierung

190 6 Die Scala-Standardbibliothek

trag Say Hello, der eine Dialogbox öffnet, in der Hello World! steht. Der Zu-stand nach Klicken des Say Hello-Menüeintrags ist in Abbildung 6.4 zu sehen.

Abbildung 6.4: Ein GUI-Window mit zwei Menüs und der Dialogbox im Vordergrund

Nachdem mit GUI-Programmierung ganze Bücher gefüllt werden können, wer-den wir nun nur noch ein letztes, etwas komplexeres Beispiel bringen und danndamit abschließen. Programmiert werden soll ein einfaches GUI zur Addition vonzwei Zahlen. Die beiden Zahlen sollen in Felder eingegeben werden können, unddahinter steht das Ergebnis. Sobald in einem Feld etwas Neues eingegeben wur-de, muss auch das Ergebnis neu berechnet werden. Abbildung 6.5 zeigt das Pro-gramm, nachdem im ersten Feld 17 und im zweiten 4 eingeben wurde.

Abbildung 6.5: Das Simple-Adder-GUI

Der gesamte Code für diese Applikation ist in Listing 6.17 dargestellt. Für die Ein-gabe der beiden Zahlen nehmen wir TextField-Objekte. Wir schreiben in das

Page 204: Scala : objektfunktionale Programmierung

6.5 Ein kleines bisschen GUI 191

erste zu Beginn eine 1 und in das zweite eine 2. Für das Ergebnis nutzen wir einLabel und setzen es am Anfang auf 3. Im MainFrame nehmen wir als contentsein FlowPanel das fünf Komponenten enthält: Die Textfelder für die beiden Zah-len, das Label für das Ergebnis und zwei weitere Labels für das + zwischen denEingabezahlen und das = vor dem Ergebnis.

Listing 6.17: Das Simple-Adder-GUI

import scala.swing._import scala.swing.event._

object SimpleAdder extends SimpleSwingApplication {val numberA = new TextField {text = "1"columns = 5

}val numberB = new TextField {text = "2"columns = 5

}val result = new Label("3")listenTo(numberA,numberB)reactions += {case EditDone(_) =>result.text = (numberA.text.toInt +

numberB.text.toInt).toString}def top = new MainFrame {title = "Simple Adder"minimumSize=new Dimension(250, 20)contents = new FlowPanel(numberA, new Label(" + "),

numberB, new Label(" = "),result)

}}

Um bei Eingaben zu reagieren, nutzen wir sogenannte Events. Vom Textfeld wirdnach der Eingabe ein Event erzeugt, auf den wir reagieren können, indem wir denText ändern, der im Label result steht.In der SimpleSwingApplication gibt es eine Methode listenTo, mit der wirfestlegen, von wem wir Events empfangen wollen. In Listing 6.17 sind das diebeiden Textfelder numberA und numberB. Außerdem gibt es die reactions,denen wir eine partielle Funktion als zusätzliche Reaktion hinzufügen können.In unserem Fall ist diese Funktion so implementiert, dass bei jedem EditDone-Event der Text von result neu berechnet wird.

Page 205: Scala : objektfunktionale Programmierung
Page 206: Scala : objektfunktionale Programmierung

Kapitel 7

Actors – Concurrency undMulticore-Programmierung

Nebenläufige Programmierung insbesondere unter Nutzung mehrerer Cores wirdimmer mehr zur Standardaufgabe für Softwareentwickler. Das liegt daran, dassdie Prozessoren nicht mehr schneller werden, sondern dass sich in einem Prozes-sor immer mehr Kerne befinden. Ein Prozessor mit sehr wenigen Kernen kannin der Regel durch Betriebssystemprozesse noch ganz ordentlich ausgenutzt wer-den. Nun können z.B. zwei Prozesse, die sonst abwechselnd rechnen durften, par-allel arbeiten. Werden es aber immer mehr Kerne, wird es essenziell notwendig,die Programme selbst in mehrere Prozesse oder Threads aufzuteilen.Nachdem Scala auf der Java Virtual Machine oder auf einer .NET-Runtime ausge-führt wird, ist natürlich alles aus Java bzw. .NET verfügbar, um nebenläufig (engl.concurrent) zu programmieren. Scala bietet aber noch eine weitere, sehr eleganteAbstraktionsmöglichkeit, die auf einem mathematischen Modell von nebenläufi-ger Programmierung basiert: die Actors1.Es gibt Implementierungen des Actor-Modells in einigen Programmiersprachenbis hin zu actorbasierten, general purpose Programmiersprachen wie z.B. ActorS-cript2. Die Scala-Implementierung lehnt sich von der Syntax stark an die Actor-Implementierung in der funktionalen Programmiersprache Erlang3 an. Actorssind ganz nach der Scala-Philosophie nicht im Sprachkern, sondern als Libraryim Package scala.actors verfügbar.Die Grundidee des Actor-Modells ist, dass ein Actor mit anderen Actors nur überMessages kommuniziert. Wird einem Actor eine Message gesendet, landet diese

1 siehe auch [Cli81] und [Agh86]2 siehe auch [Hew09] und http://actorscript.org/3 siehe auch [Arm07] und http://www.erlang.org/

Page 207: Scala : objektfunktionale Programmierung

194 7 Actors – Concurrency und Multicore-Programmierung

in seiner Mailbox und bleibt dort, bis der Actor sie herausnimmt. Das heißt also,die Kommunikation ist grundsätzlich asynchron4.In diesem Kapitel geben wir Ihnen eine Einführung in die Welt der Scala-Actors.Wir beginnen in Abschnitt 7.1 damit, den Zusammenhang zwischen Actors undThreads zu erläutern. Wie ein Actor Messages empfangen und darauf reagierenkann, wird in Abschnitt 7.2 dargestellt. Besondere Aktoren, nämlich Reaktorenund Dämonen, sind Gegenstand von Abschnitt 7.3. Für Aktoren wird ein Sche-duling benötigt. Informationen darüber finden Sie in Abschnitt 7.4. Das Kapitelschließt mit einem Blick auf Remote Actors (siehe Abschnitt 7.5).

7.1 Ein Thread ist ein Actor

In Scala ist jeder Thread auch ein Actor. Die Umkehrung gilt allerdings nicht, danicht jeder Actor einen eigenen Thread benötigt. Um nebenläufig eine Aufgabezu erledigen, nutzen wir in Scala üblicherweise einen Actor. Die einfachste Mög-lichkeit ist die actor-Methode des scala.actors.Actor-Companion-Objekts.Listing 7.1 zeigt ein Beispiel, bei dem neben dem Main-Thread ein zweiter Threaddurch die actor-Methode erzeugt wird.

Listing 7.1: Das Objekt TwoThreads, das einen Thread mit der Methode actor erzeugt

object TwoThreads {

def main(args: Array[String]) {

import scala.actors.Actor.actor

println("Main: gestartet")

actor {println("Actor: gestartet")for (i <- 1 to 4) {Thread.sleep(1000)println("Actor: "+i)

}println("Actor: fertig")

}

for (i <- 1 to 4) {Thread.sleep(1000)println("Main: "+i)

}

println("Main: fertig")}

}

4 Wie wir später sehen werden, gibt es auch die Möglichkeit, synchron zu kommunizieren.

Page 208: Scala : objektfunktionale Programmierung

7.1 Ein Thread ist ein Actor 195

Der Parameter der Funktion actor hat den Typ => Unit. Es ist ein By-Name-Parameter, der erst in der actor-Funktion ausgeführt wird. Importieren wir dieMethode aus dem Actor-Objekt und nutzen geschweifte Klammern für das Ar-gument, so sieht actor sogar wie eine Kontrollstruktur aus. Alles, was im Argu-ment steht, wird also in einem eigenen Thread ausgeführt. Führen wir das ObjektTwoThreads aus, sehen wir etwas dem Folgenden Ähnliches auf der Konsole:

Main: gestartetActor: gestartetActor: 1Main: 1Actor: 2Main: 2Actor: 3Main: 3Main: 4Actor: 4Main: fertigActor: fertig

Obwohl beide Threads immer wieder 1000 Millisekunden schlafen, ist die Aus-führungsreihenfolge nicht deterministisch. Wie in der obigen Ausgabe zu sehenist, sind die Ausgaben der beiden Threads nicht streng abwechselnd. Das heißtalso, das Verhalten des Gesamtprogramms ist nicht deterministisch. Willkommenin der Welt der Threads!Wie wir gesehen haben, wird der mit actor erzeugte Actor auch sofort gestartet.Das ist in diesem Fall auch sehr angenehm, es gibt aber auch die Möglichkeit,einen Actor zu definieren, der explizit gestartet werden muss. Dazu nutzen wirden Trait Actor.

Listing 7.2: Das Objekt ThreeThreads mit der inneren Klasse MyActor

object ThreeThreads {import scala.actors.Actor

class MyActor(number: Int) extends Actor {def act() {println("Actor"+number+": gestartet")for (i <- 1 to 2) {Thread.sleep(1000)println("Actor"+number+": "+i)

}println("Actor"+number+": fertig")

}}

def main(args: Array[String]) {

println("Main: gestartet")

Page 209: Scala : objektfunktionale Programmierung

196 7 Actors – Concurrency und Multicore-Programmierung

for (i <- 1 to 3)new MyActor(i).start()

for (i <- 1 to 2) {Thread.sleep(1000)println("Main: "+i)

}

println("Main: fertig")}

}

In Listing 7.2 definieren wir die Klasse MyActor, die den Actor-Trait hinzufügt.Im Actor-Trait gibt es eine einzige abstrakte Methode, die implementiert werdenmuss:

def act(): Unit

Der Rumpf der act-Methode wird ausgeführt, sobald der Actor mit der start-Methode gestartet wird. In Listing 7.2 erzeugen wir 3 MyActor-Objekte, die wiralle sofort starten. Die Ausgabe könnte dann folgendermaßen aussehen:

Main: gestartetActor2: gestartetActor3: gestartetActor1: gestartetMain: 1Actor2: 1Actor1: 1Actor3: 1Main: 2Main: fertigActor2: 2Actor2: fertigActor1: 2Actor3: 2Actor1: fertigActor3: fertig

7.2 Empfangen und Reagieren

Solange Threads voneinander unabhängig sind, sind sie auch mit anderen Kon-zepten einfach zu erzeugen. Schwieriger wird es, sobald die Threads auf irgend-eine Weise miteinander zusammenarbeiten sollen. In Programmiersprachen wieJava wird dies üblicherweise mit gemeinsam genutzten Daten gemacht, die dannThread-sicher gemacht werden müssen.Nach dem Actor-Modell werden keine Daten gemeinsam genutzt. Kommunika-tion, und darüber die Zusammenarbeit, funktioniert über Messages. Dabei kann

Page 210: Scala : objektfunktionale Programmierung

7.2 Empfangen und Reagieren 197

eine Message ein beliebiges Objekt sein. Üblicherweise werden Case-Klassen und-Objekte bzw. Strings, Tupel etc. genutzt, um mit Pattern Matching auf die Mes-sage reagieren zu können.Zum Senden einer Message gibt es die !-Methode im Actor-Trait. Das heißt, mitdem Ausdruck actor ! message wird dem Actor mit dem Bezeichner actordie Message message gesendet. Die Message landet in der Mailbox des Actorsund bleibt dort, bis der Actor sie herausnimmt.Nachdem jeder Thread ein Actor ist, können wir dem Hauptprozess der Scala-Shell selbst eine Message senden, z.B. mit:

scala> import scala.actors.Actor._import scala.actors.Actor._

scala> self ! "Hello"

Der Bezeichner self entspricht einer Methode aus dem Actor-Companion-Objekt, die den Actor, der gerade ausgeführt wird, zurückgibt. Die Message"Hello" befindet sich nun in der Mailbox des Actors. Mit der Methode:

def receive[A](f: PartialFunction[Any, A]): A

aus dem Actor-Trait können wir die Message empfangen und darauf reagieren.In unserer interaktiven Sitzung könnte das beispielsweise folgendermaßen ausse-hen:

scala> receive {| case msg => println("Received message: "+msg)| }

Received message: Hello

Wie im Typ der receive-Methode zu sehen ist, ist das Argument eine partiel-le Funktion. Das heißt, wenn wir eine Message in der Mailbox finden, die nichtzu den in receive definierten Fällen passt, wird die Message einfach ignoriert.Senden wir dem Actor beispielsweise einen Int, obwohl er nur auf Stringsreagiert, stürzt das Programm nicht ab, sondern wartet weiter, bis eine passendeMessage kommt. In der folgenden Sitzung wird dies gemacht:

scala> self ! 1

scala> receive {| case msg: String| => println("Received message: "+msg)| }

Wenn Sie die letzte Sitzung selbst ausprobieren, werden Sie feststellen, dass dieShell unendlich wartet und kein Prompt mehr erscheint. Die Methode receiveblockiert solange, bis eine passende Nachricht aus der Mailbox entnommen wer-

Page 211: Scala : objektfunktionale Programmierung

198 7 Actors – Concurrency und Multicore-Programmierung

den kann, d.h. potenziell unendlich. Nachdem Sie an die Shell sonst auch nichtherankommen, müssen Sie es irgendwann explizit abbrechen5.Für das Empfangen einer Message kann mit receiveWithin auch ein Timeout-Intervall angegeben werden. Nach Ablauf der angegebenen Zeitspanne wird demActor als Message das scala.actors.TIMEOUT-Objekt gesendet. Der Actorkann dann darauf reagieren. Andernfalls wird eine Exception geworfen.

Listing 7.3: Das Objekt Timeout mit einer Klasse MyActor, die die Methode receive-Within nutzt

object Timeout {

import scala.actors._import scala.actors.Actor._

class MyActor(timeout: Long) extends Actor {def act() {println("Actor started, waiting "+

timeout+"ms for strings")receiveWithin(timeout) {case msg: String =>println("Actor: received "+msg)

case TIMEOUT =>println("Actor: TIMEOUT")

}}

}

def main(args: Array[String]) {

var myActor = new MyActor(100)myActor.start()myActor ! "Hello"

myActor = new MyActor(0)myActor ! "Hello Again"myActor.start()

myActor = new MyActor(100)myActor.start()

}}

Die Klasse MyActor in Listing 7.3 nutzt receiveWithin und reagiert mit einemeigenen Fall auf das TIMEOUT-Objekt. Wird das Objekt ausgeführt, sehen wir einesolche oder ähnliche Ausgabe:

Actor started, waiting 100ms for strings

5 z.B. mit Strg-C

Page 212: Scala : objektfunktionale Programmierung

7.2 Empfangen und Reagieren 199

Actor started, waiting 0ms for stringsActor started, waiting 100ms for stringsActor: received Hello AgainActor: received HelloActor: TIMEOUT

Das TIMEOUT-Objekt ist ein ganz normales Objekt. Daher müssen wir auch nichtmit einem speziellen Fall reagieren. Ersetzen wir beispielsweise den receive-Within-Block aus Listing 7.3 wie folgt:

receiveWithin(timeout) {case msg => println("Actor: received "+msg)

}

und reagieren damit auf jede Message und nicht mehr nur auf msg: String,bekommen wir als letzte Ausgabe:

Actor: received TIMEOUT

Wir können mit einem Actor auch auf mehrere Messages reagieren. In Listing 7.4ist ein Actor dargestellt, der zuerst einen String, danach einen Int als Messageverarbeiten will. Dabei ist es nicht wichtig, welche Message als Erstes ankommt.Der Actor blockiert solange, bis er eine String-Message bekommen hat. Dieanschließend benötigte Int-Message ist bereits vor der String-Message in seinerMailbox angekommen und kann dann als Zweites entnommen werden.

Listing 7.4: Das Objekt TwoMessagesmit einem Actor, der zwei receive-Blöcke enthält

object TwoMessages {import scala.actors._import scala.actors.Actor._

class MyActor extends Actor {def act() {receive {case msg: String =>println("Actor: got String: "+msg)

}receive {case i: Int =>println("Actor: got Int: "+i)

}}

}def main(args: Array[String]) {val myActor = new MyActormyActor.start()myActor ! 1myActor ! "Hello"

}}

Page 213: Scala : objektfunktionale Programmierung

200 7 Actors – Concurrency und Multicore-Programmierung

Führen wir das Objekt TwoMessages aus, so sehen wir folgende Ausgabe auf derKonsole:

Actor: got String: HelloActor: got Int: 1

Wir können auch mit einem receive-Block in einer Schleife auf mehrere Messa-ges hintereinander reagieren. In Listing 4.7 auf Seite 57 hatten wir ein Shared-Counter-Objekt definiert, das einen Counter repräsentiert, der durch alle Objekteder Klasse SharedCounter gemeinsam genutzt wird. In Listing 7.5 zeigen wirdenselben Ansatz mit einem Actor.

Listing 7.5: Eine Applikation mit einem SharedCounter-Actor

object Counter {

import scala.actors._import scala.actors.Actor._

case object Incrementcase object Valuecase object Stop

object SharedCounter extends Actor {private[this] var count = 0def act() {var continue = truewhile(continue) {receive {case Increment => count +=1case Value => sender ! countcase Stop => continue = false

}}

}}

def printValueOfSharedCounter() = {SharedCounter ! ValuereceiveWithin(100) {case i: Int =>println("value of shared counter: "+i)

case TIMEOUT =>println("shared counter not available")

}}

def main(args: Array[String]) {

SharedCounter ! IncrementSharedCounter ! IncrementSharedCounter.start()

Page 214: Scala : objektfunktionale Programmierung

7.2 Empfangen und Reagieren 201

printValueOfSharedCounter()SharedCounter ! IncrementprintValueOfSharedCounter()SharedCounter ! StopSharedCounter ! IncrementprintValueOfSharedCounter()SharedCounter.restart()printValueOfSharedCounter()SharedCounter ! Stop

}}

Zunächst definieren wir drei Case-Objekte für die verschiedenen Messages. DasSharedCounter-Objekt hat eine Variable count, mit der gezählt wird, und de-finiert die Methode act. Dieses Mal soll aber nicht nur eine Message empfangenwerden, sondern alle Messages bis zur Message Stop. Dies erledigen wir mit ei-ner while-Schleife und einem Flag.Beim Empfangen der Message Increment wird count inkrementiert. Bei Valuewird dem Sender der Message, der mit der Methode sender ermittelt werdenkann, der aktuelle Wert von count zurückgesendet. Bei Stop wird continueauf false gesetzt und damit die Methode act und der gesamte Actor beendet.Die Methode printValueOfSharedCounter dient zum Abfragen und Aus-geben des aktuellen SharedCounter-Wert. Nachdem wir bei einem beendetenSharedCounter nicht unendlich warten wollen, nutzen wir receiveWithinund definieren eine Reaktion auf TIMEOUT.In der main-Methode schließlich inkrementieren, starten und stoppen wir denSharedCounter. Ein bereits gestarteter und gestoppter Actor kann mit re-start erneut gestartet werden. Außerdem fragen wir viermal den aktuellenStand ab. Führen wir das Count-Objekt aus, sehen wir auf der Konsole folgen-de Ausgaben:

value of shared counter: 2value of shared counter: 3shared counter not availablevalue of shared counter: 4

Obwohl der SharedCounter erst nach dem Senden von zwei Increment-Messages gestartet wurde, gingen die Nachrichten nicht verloren. Auch nach demStoppen können wir noch weiter Messages senden, wir bekommen aber keineAntwort mehr. Nach dem erneuten Starten verarbeitet der Actor die zwischen-zeitlich eingetroffenen Messages.Ein Nachteil des SharedCounter-Ansatzes mit einem Actor, wie wir ihn in Lis-ting 7.5 vorgestellt haben, ist die Tatsache, dass wir für jede Message, die derActor verarbeitet, in den Thread des Actors wechseln müssen. Dies bedeutetOverhead und, wenn wir beispielsweise mehrere solcher Actors nutzen, Res-sourcenverschwendung, weil jeder einen eigenen Thread nutzt. Wir können dies

Page 215: Scala : objektfunktionale Programmierung

202 7 Actors – Concurrency und Multicore-Programmierung

zunächst einmal durch Einstreuen von Thread.currentThread-Ausgaben ver-deutlichen.

Listing 7.6: Eine Applikation mit vielen SharedCounter-Actor und Ausgaben, welcherThread gerade läuft

object Counter2 {

import scala.actors._import scala.actors.Actor._

case object Incrementcase object Stop

class SharedCounter(i: Int) extends Actor {private[this] var count = 0def act() {var continue = truewhile(continue) {receive {case Increment => {println("Actor "+i+": "+Thread.currentThread)count +=1

}case Stop => continue = false

}}

}}

def main(args: Array[String]) {

val counters = for (i <- 0 to 9)yield new SharedCounter(i)

for (i <- 0 to 9) counters(i).start()for (i <- 0 to 9) counters(i) ! Incrementfor (i <- 0 to 9) counters(i) ! IncrementThread.sleep(1000)for (i <- 0 to 9) counters(i) ! Stop

}}

Listing 7.6 zeigt eine modifizierte Version von Listing 7.5. Statt ein ObjektSharedCounter definieren wir eine Klasse SharedCounter. Der act-Methodehaben wir in der Reaktion auf die Increment-Message eine Ausgabe des aktu-ellen Threads hinzugefügt. Die Rückgabe des Wertes von count und die Aus-gabe haben wir aus Gründen der Übersichtlichkeit weggelassen. Um die erzeug-ten SharedCounter unterscheiden zu können, verwenden wir einen Klassen-Parameter, den wir dann jeweils mit ausgeben.

Page 216: Scala : objektfunktionale Programmierung

7.2 Empfangen und Reagieren 203

Wir erzeugen zehn SharedCounter-Objekte, starten diese und senden jedemzweimal die Message Increment. Bevor wir sie stoppen, legen wir den main-Thread eine Weile schlafen. Würden wir das nicht tun, könnten Threads aus demThreadpool wiederverwendet werden, da ein Actor bereits beendet sein kann,bevor ein anderer überhaupt gestartet wird6.Führen wir das Objekt Counter2 aus, sehen wir folgende oder eine ähnliche Aus-gabe auf der Konsole:

Actor 0: Thread[ForkJoinPool-1-worker-3,5,main]Actor 1: Thread[ForkJoinPool-1-worker-1,5,main]Actor 2: Thread[ForkJoinPool-1-worker-2,5,main]Actor 3: Thread[ForkJoinPool-1-worker-0,5,main]Actor 0: Thread[ForkJoinPool-1-worker-3,5,main]Actor 3: Thread[ForkJoinPool-1-worker-0,5,main]Actor 2: Thread[ForkJoinPool-1-worker-2,5,main]Actor 1: Thread[ForkJoinPool-1-worker-1,5,main]Actor 4: Thread[ForkJoinPool-1-worker-4,5,main]Actor 4: Thread[ForkJoinPool-1-worker-4,5,main]Actor 5: Thread[ForkJoinPool-1-worker-5,5,main]Actor 5: Thread[ForkJoinPool-1-worker-5,5,main]Actor 6: Thread[ForkJoinPool-1-worker-6,5,main]Actor 6: Thread[ForkJoinPool-1-worker-6,5,main]Actor 7: Thread[ForkJoinPool-1-worker-7,5,main]Actor 7: Thread[ForkJoinPool-1-worker-7,5,main]Actor 8: Thread[ForkJoinPool-1-worker-8,5,main]Actor 8: Thread[ForkJoinPool-1-worker-8,5,main]Actor 9: Thread[ForkJoinPool-1-worker-9,5,main]Actor 9: Thread[ForkJoinPool-1-worker-9,5,main]

Das Wesentliche an der Ausgabe ist, dass jeder Actor einen eigenen Thread hat.Es tauchen in der Ausgabe also zehn verschiedene Threads auf.Scala bietet eine sehr einfache7 Möglichkeit, Actors so zu programmieren,dass sie keinen eigenen Thread benötigen. Wir müssen einfach react bzw.reactWithin statt receive bzw. receiveWithin nutzen. Die Grundidee ist,dass die Reaktion auf eine Message vom Kontext entkoppelt ist. Das heißt, es müs-sen nur einige Anweisungen ausgeführt, aber kein Kontext hergestellt werden.Damit kann einfach irgendein beliebiger Thread genutzt werden, um die Anwei-sungen auszuführen.Eine aus dem Ansatz resultierende Besonderheit ist, dass der Aufruf von reactniemals zurückkehrt. Das bedeutet, react gibt im Gegensatz zu receive keinErgebnis zurück, und Code, der nach dem react steht, wird niemals ausgeführt.Insbesondere heißt das aber auch, dass ein react nicht in eine Schleife eingebettetwerden kann. Das Schleifenende wird nie erreicht, und damit kann es auch keinenzweiten Durchlauf der Schleife geben.

6 Wir bewegen uns in der Welt der Nebenläufigkeit. Im main-Thread schreiben wir verschiedene Din-ge mit verschiedenen Threads hintereinander. Die Ausführungsreihenfolge der Threads ist aber nachwie vor nicht deterministisch.7 Einfach zu nutzen, doch die Implementierung ist eher komplex.

Page 217: Scala : objektfunktionale Programmierung

204 7 Actors – Concurrency und Multicore-Programmierung

Listing 7.7: Eine Applikation mit vielen SharedCounter-Actors, die react nutzen

object Counter3 {

import scala.actors._import scala.actors.Actor._

case object Incrementcase object Stop

class SharedCounter(i: Int) extends Actor {def act() = act(0)def act(count: Int) {react {case Increment => {println("Actor "+i+": "+Thread.currentThread)act(count+1)

}case Stop =>

}// this will never be executedprintln("Actor "+i+" stopped")

}}

def main(args: Array[String]) {

val counters = for (i <- 0 to 9)yield new SharedCounter(i)

for (i <- 0 to 9) counters(i).start()for (i <- 0 to 9) counters(i) ! Incrementfor (i <- 0 to 9) counters(i) ! IncrementThread.sleep(1000)for (i <- 0 to 9) counters(i) ! Stop

}}

In Listing 7.7 haben wir den Code der Klasse SharedCounter so abgeändert,dass statt receive die Methode react genutzt wird. Nachdem wir nicht bei derwhile-Schleife bleiben können, rufen wir act rekursiv wieder auf, da wir fürden Zähler keine veränderbare Variable mehr nutzen können. Daher definierenwir eine Methode act, die den aktuellen Zählerstand als Argument bekommt.Wir starten mit dem Zählerstand 0. Wird die Message Stop empfangen, müssenwir nichts tun. Wichtig ist aber, dass wir auf Stop reagieren, sonst würde derActor weiter warten. Führen wir das Objekt Counter3 aus, sehen wir, dass derprintln-Ausdruck nach dem react-Block nie ausgewertet wird:

Actor 0: Thread[ForkJoinPool-1-worker-1,5,main]Actor 3: Thread[ForkJoinPool-1-worker-3,5,main]Actor 0: Thread[ForkJoinPool-1-worker-1,5,main]Actor 3: Thread[ForkJoinPool-1-worker-3,5,main]

Page 218: Scala : objektfunktionale Programmierung

7.2 Empfangen und Reagieren 205

Actor 4: Thread[ForkJoinPool-1-worker-1,5,main]Actor 5: Thread[ForkJoinPool-1-worker-3,5,main]Actor 4: Thread[ForkJoinPool-1-worker-1,5,main]Actor 5: Thread[ForkJoinPool-1-worker-3,5,main]Actor 6: Thread[ForkJoinPool-1-worker-1,5,main]Actor 7: Thread[ForkJoinPool-1-worker-3,5,main]Actor 6: Thread[ForkJoinPool-1-worker-1,5,main]Actor 7: Thread[ForkJoinPool-1-worker-3,5,main]Actor 8: Thread[ForkJoinPool-1-worker-1,5,main]Actor 9: Thread[ForkJoinPool-1-worker-3,5,main]Actor 8: Thread[ForkJoinPool-1-worker-1,5,main]Actor 9: Thread[ForkJoinPool-1-worker-3,5,main]Actor 2: Thread[ForkJoinPool-1-worker-2,5,main]Actor 1: Thread[ForkJoinPool-1-worker-0,5,main]Actor 1: Thread[ForkJoinPool-1-worker-0,5,main]Actor 2: Thread[ForkJoinPool-1-worker-0,5,main]

Außerdem können wir in der obigen Ausgabe feststellen, dass für alle zehnActors nur vier Threads genutzt wurden. Das heißt, der Ansatz mit react benö-tigt deutlich weniger Ressourcen. Wir können das Ganze sogar noch auf die Spitzetreiben und alle Actors im main-Thread laufen lassen (siehe Abschnitt 7.4). Wirsehen außerdem auch, dass zum Beispiel Actor 2 für die beiden unabhängigenReaktionen zwei verschiedene Threads nutzt.Der vorgestellte Ansatz mit react ist sogar auch etwas schöner, weil keine ver-änderlichen Variablen genutzt werden. Gibt es aber mehrere Fälle in dem react-Block, die mit einem rekursiven Aufruf enden, birgt das die potenzielle Gefahr,einen davon zu vergessen. Als Ausweg stellt die Actor-Klasse die beiden Metho-den loop und loopWhile zur Verfügung. Damit können wir den Code wiederetwas imperativer aussehen lassen8.

Listing 7.8: Die Klasse SharedCounter mit react und loopWhile

class SharedCounter(i: Int) extends Actor {private[this] var count = 0def act() {var continue = trueloopWhile(continue) {react {case Increment => {println("Actor "+i+": "+Thread.currentThread)count += 1

}case Stop => continue = false

}}println("Actor "+i+" stopped") // never reached

}}

8 ... wenn wir das wirklich wollen ;-).

Page 219: Scala : objektfunktionale Programmierung

206 7 Actors – Concurrency und Multicore-Programmierung

Listing 7.8 zeigt die Klasse SharedCounter unter Verwendung von react undloopWhile. Der Code sieht fast aus wie zuvor in Listing 7.6, wobei while durchloopWhile und receive durch react ersetzt wurde. Aber auch hier wird dieAusgabe nach dem loopWhile-Block nie ausgeführt.Bisher haben wir nur über asynchrone Messages mit einem Actor kommuniziert.Auch eine Antwort kam als Message, die wir explizit mit receive oder reactempfangen mussten. Es gibt noch eine Möglichkeit, die Antwort direkt einem Be-zeichner zuzuweisen, obwohl sie noch gar nicht vorhanden ist. Nutzen wir zumSenden statt der Methode ! die Methode !!, bekommen wir ein Future-Objektzurück. Dieses kapselt den Wert, der als Antwort in der Zukunft zurückkommenwird. Am Actor selbst müssen wir nichts verändern.

Listing 7.9: Eine Applikation mit einem SharedCounter-Objekt und Zugriff auf die Ant-wort über ein Future

object FutureCounter {

import scala.actors._import scala.actors.Actor._

case object Incrementcase object Valuecase object Stop

object SharedCounter extends Actor {private[this] var count = 0def act() {var continue = truewhile (continue) {receive {case Increment => count+=1case Value => {println("SharedCounter is sleeping")Thread.sleep(1000)println("SharedCounter is awake")sender ! count

}case Stop => continue = false

}}

}}

def main(args: Array[String]) {

SharedCounter.start()SharedCounter ! IncrementSharedCounter ! Incrementval value = SharedCounter !! Valueprintln("Value-request sent")

Page 220: Scala : objektfunktionale Programmierung

7.3 Dämonen und Reaktoren 207

println("Value = "+value)println("Value = "+value())SharedCounter ! Stop

}}

In Listing 7.9 ist der Zugriff auf die Antwort über ein Future-Objekt dargestellt.Wir nutzen dasselbe SharedCounter-Objekt wie in Listing 7.5 mit einer Ergän-zung von drei Zeilen in der Reaktion auf das Value-Objekt. Diese Ergänzungdient ausschließlich der Veranschaulichung. Der Actorwird mit Thread.sleepschlafen gelegt, wobei davor und danach Statusmeldungen auf der Konsole aus-gegeben werden.In der main-Methode senden wir dem SharedCounter die Message Value mitder !!-Methode und weisen das Ergebnis dem Bezeichner value zu. Anschlie-ßende geben wir value mit und ohne leeres Klammerpaar aus. Lassen wir dieApplikation laufen, sehen wir folgende Ausgabe:

Value-request sentValue = <function0>SharedCounter is sleepingSharedCounter is awakeValue = 2

Die Ausgabe von value ohne geschweifte Klammern erfolgt unmittelbar, undzwar als <function0>. Das heißt, es handelt sich bei value um eine 0-stelligeFunktion. Nutzen wir value(), so wird eigentlich value.apply() ausgewer-tet. Nachdem es sich um ein Future handelt, blockiert dieser Aufruf solange, bisder Wert verfügbar ist, also bis der SharedCounter aufgewacht ist, die Antwortgesendet hat und diese in der Mailbox abrufbar ist. Mit value.isSet kann üb-rigens überprüft werden, ob der Wert schon verfügbar ist.

7.3 Dämonen und Reaktoren

Scala bietet auch noch einige spezielle Actors. Der DaemonActor bietet dasselbeInterface wie der Actor. Der Unterschied ist, dass eine Applikation auch dannbeendet wird, wenn es noch laufende DaemonActors gibt. Auf laufende Actorswird gewartet. Das heißt, wenn wir die SharedCounter-Objekte nicht alle mitder Message Stop beendet hätten, könnte die Applikation insgesamt nie beendetwerden.Nachdem ein SharedCounter ein Hilfsobjekt ist, das nicht zum Selbstzweckexistiert, ist es durchaus sinnvoll, es als DaemonActor zu implementieren. In Lis-ting 7.10 ist eine Implementierung des SharedCounters als DaemonActor.

Page 221: Scala : objektfunktionale Programmierung

208 7 Actors – Concurrency und Multicore-Programmierung

Listing 7.10: Eine Applikation mit vielen SharedCounter-DaemonActors

object Counter5 {

import scala.actors._import scala.actors.Actor._

case object Incrementcase object Value

object SharedCounter extends DaemonActor {def act() = act(0)def act(count: Int) {react {case Increment => act(count+1)case Value => {sender ! countact(count)

}}

}}

def main(args: Array[String]) {

SharedCounter.start()SharedCounter ! IncrementSharedCounter ! IncrementSharedCounter ! Valuereceive {case i => println("Value = "+i)

}

}}

Der SharedCounter ist dieses Mal wieder direkt als Objekt implementiert.Nachdem es ein DaemonActor ist, müssen wir es nicht extra beenden. Wir startenden SharedCounter und nutzen ihn. Obwohl er noch läuft, wird das Programmnach Ausgabe von Value 2 erfolgreich beendet. Wir müssen ihn also nicht expli-zit beenden, z.B. mit einer Stop-Message.Ein Reactor ist ein sehr leichtgewichtiger Actor. Die abweichenden Merkmaledes Reactors gegenüber dem Actor sind:

Der Sender wird nicht implizit mit der Message übertragen.

Es gibt nur react, kein receive, d.h. keinen exklusiven Thread.

Der Reactor verwaltet weniger Zustand als der Actor.

Soll doch ein Sender übertragen werden, kann der Subtrait ReplyReactor ge-nutzt werden. Der Trait Reactor benötigt noch einen Typparameter, für den als

Page 222: Scala : objektfunktionale Programmierung

7.4 Scheduler 209

Lower Bound Null spezifiziert ist. Ansonsten können wir den Reactor analogzu einem Actor nutzen.

Listing 7.11: Eine Applikation mit einem StringBuildReactor

object StringBuildReactor {

import scala.actors.Reactorimport scala.actors.Actor._

object MyStringBuildReactor extends Reactor[String] {def act() = act(new StringBuilder)def act(str: StringBuilder) {react {case "" => println(str)case s => act(str ++= s)

}}

}

def main(args: Array[String]) {

MyStringBuildReactor.start()"Hello world and hello reader!" split" " map{str => str.head.toUpper + str.tail} foreach{MyStringBuildReactor ! _}

MyStringBuildReactor ! ""

}}

In Listing 7.11 ist ein Beispiel für einen Reactor dargestellt. Der MyString-BuildReactor empfängt nur Strings und hängt diese mit einem String-Builder aneinander. Wird der leere String gesendet, gibt er die gesamte Zei-chenkette aus und beendet sich.Nachdem der Reactor gestartet wurde, werden die Wörter aus dem angegebe-nen String einzeln gesendet. Vorher wird der jeweils erste Buchstabe in einenGroßbuchstaben umgewandelt. Anschließend wird noch der leere String gesen-det. Die Ausgabe lautet dann:

HelloWorldAndHelloReader!

7.4 Scheduler

Im Package scala.actors.scheduler sind einige Scheduler definiert. Wie wirin einigen Ausgaben in Abschnitt 7.2 gesehen haben, wird standardmäßig der

Page 223: Scala : objektfunktionale Programmierung

210 7 Actors – Concurrency und Multicore-Programmierung

ForkJoinScheduler verwendet. Dieser Scheduler verwaltet eine Menge vonThreads (Thread-Pool), in denen die einzelnen Actors ablaufen können.In Listing 7.7 auf Seite 204 haben wir mit react die Actors dazu gebracht, nurwenige Threads zu nutzen. In der Beispielausgabe waren es vier Threads. Rede-finieren wir die scheduler-Methode aus dem Trait Actor mit einem Single-ThreadedScheduler-Objekt, werden alle Actors in nur einem Thread laufen.

Listing 7.12: Eine Applikation mit vielen SharedCounter-Actors, die in einem Threadlaufen

object Counter6 {

import scala.actors._import scala.actors.Actor._

case object Incrementcase object Stop

class SharedCounter(i: Int) extends Actor {import scala.actors.scheduler.SingleThreadedScheduleroverride def scheduler = new SingleThreadedSchedulerdef act() = act(0)def act(count: Int) {react {case Increment => {println("Actor "+i+": "+Thread.currentThread)act(count+1)

}case Stop =>

}}

}

def main(args: Array[String]) {

val counters = for (i <- 0 to 9)yield new SharedCounter(i)

for (i <- 0 to 9) counters(i).start()for (i <- 0 to 9) counters(i) ! Incrementfor (i <- 0 to 9) counters(i) ! IncrementThread.sleep(1000)for (i <- 0 to 9) counters(i) ! Stop

}}

In Listing 7.12 haben wir die scheduler-Methode redefiniert. Lassen wir dieCounter6-Applikation laufen, sehen wir eine Ausgabe wie die folgende:

Actor 0: Thread[main,5,main]Actor 1: Thread[main,5,main]

Page 224: Scala : objektfunktionale Programmierung

7.5 Remote Actors 211

Actor 2: Thread[main,5,main]Actor 3: Thread[main,5,main]Actor 4: Thread[main,5,main]Actor 5: Thread[main,5,main]Actor 6: Thread[main,5,main]Actor 7: Thread[main,5,main]Actor 8: Thread[main,5,main]Actor 9: Thread[main,5,main]Actor 0: Thread[main,5,main]Actor 1: Thread[main,5,main]Actor 2: Thread[main,5,main]Actor 3: Thread[main,5,main]Actor 4: Thread[main,5,main]Actor 5: Thread[main,5,main]Actor 6: Thread[main,5,main]Actor 7: Thread[main,5,main]Actor 8: Thread[main,5,main]Actor 9: Thread[main,5,main]

Das heißt, alle Actors laufen im main-Thread. Neben den beiden vorgestelltengibt es noch die folgenden Scheduler:

die ResizableThreadPoolScheduler die, wie der Name schon sagt, einenThread-Pool nutzen. Der Thread-Pool wird bei Bedarf vergrößert, z.B. wennviele Actors in einem eigenen Thread laufen sollen.

Der DaemonScheduler als Standardscheduler für die DaemonActors.

Statt in jedem Actor die scheduler-Methode zu redefinieren, kann die JVM-Property actors.enableForkJoin auf false gesetzt werden. Damit wird derResizableThreadPoolScheduler zum Standardscheduler für Actors. Au-ßerdem gibt es noch die Property actors.corePoolSize zum Verändern derGröße des genutzten Thread-Pools.

7.5 Remote Actors

Mit dem Package scala.actors.remote können Actors auf einfache Weiseüber das Netzwerk zur Verfügung gestellt werden. Dazu definiert das ObjektRemoteActor einige Methoden, die wir im Folgenden vorstellen wollen.Um einen Actor an einen Port zu binden, gibt es die Methode alive. Die Me-thode register registriert einen Actor unter einem Namen. Auf diese Weisekönnen mehrere Actors unter einem Port mit verschiedenen Namen angespro-chen werden.

Page 225: Scala : objektfunktionale Programmierung

212 7 Actors – Concurrency und Multicore-Programmierung

Listing 7.13: Das Objekt RemoteActor, das zwei Actors unter verschiedenen Namen aufPort 9010 verfügbar macht

case object Stop

object RemoteActor {

import scala.actors.Actor.{actor, loopWhile, react, self}

import scala.actors.remote.RemoteActor.{alive, register}

def remoteActor(name: String, remoteName: Symbol) =actor {alive(9010)register(remoteName, self)println(name+": Waiting for messages...")var continue = trueloopWhile(continue) {react {case Stop => {println(name+": Exiting")continue = false

}case i: Int => println(name+": "+i+": Int")case msg => println(name+": "+msg)

}}

}

def main(args: Array[String]) {

remoteActor("Echo1",‘echo1)remoteActor("Echo2",‘echo2)

}}

Ein Beispiel für zwei RemoteActors ist in Listing 7.13 dargestellt. Um denActor stoppen zu können, definieren wir ein Case-Objekt Stop. Dasselbe defi-nieren wir weiter unten auch im Client-Code.Um zwei gleichartige Actors zu nutzen, implementieren wir eine remoteActor-Methode, der wir den Namen (zur lokalen Ausgabe) und den Netzwerknamen(zur Registrierung) übergeben. Der Netzwerkname muss ein Symbol sein. DerActor wird durch die Verwendung der actor-Methode sofort gestartet. Er wirdunter dem Port 9010 und dem übergebenen Netzwerknamen registriert.Anschließend gibt er eine Statusmeldung aus und wartet auf Messages. Dabeiunterscheidet er zwischen dem Objekt Stop, Ints und allen anderen Messages.Starten wir das Programm, sehen wir die beiden Zeilen

Page 226: Scala : objektfunktionale Programmierung

7.5 Remote Actors 213

Echo2: Waiting for messages...Echo1: Waiting for messages...

in dieser Reihenfolge oder umgekehrt auf der Konsole. Nachdem auf Messagesgewartet wird, kehrt die Shell nicht zum Prompt zurück.Auf der Client-Seite nutzen wir die Methode select, um auf einen Remote-Actor zugreifen zu können. Als Argumente benötigen wir einen Node mit Netz-werknamen oder IP-Adresse und dem Port sowie den Namen, unter dem derRemoteActor registriert ist, wieder als Symbol.

Listing 7.14: Das Objekt SendToRemoteActor, das Messages an zwei verschiedeneRemoteActors unter verschiedenen Namen auf demselben Node sendet

case object Stop

object SendToRemoteActor extends Application {

import scala.actors.Actor.actorimport scala.actors.remote.Nodeimport scala.actors.remote.RemoteActor.select

val node = Node("127.0.0.1", 9010)

actor {val c = select(node, ‘echo1)val d = select(node, ‘echo2)

c ! "Hello RemoteActor"d ! "Hello RemoteActor"for (i <- 1 to 10) {c ! id ! i

}c ! Stopd ! Stop

}

}

In Listing 7.14 haben wir die Client-Seite dargestellt. Um die RemoteActors stop-pen zu können, wird zuerst das Case-Objekt Stop definiert. In der Applikationwird ein Actor mit actor erzeugt, der den RemoteActors Messages sendet.Um einen RemoteActor zu nutzen, wird die Methode select mit dem Nodeund dem Namen als Argumente genutzt. Anschließend werden den beiden ab-wechselnd einige Messages gesendet. Nachdem beide Stop empfangen haben,wird die Applikation, die die RemoteActors bereitstellt, beendet. Davor wurdenoch Folgendes ausgegeben:

Echo2: Hello RemoteActorEcho1: Hello RemoteActor

Page 227: Scala : objektfunktionale Programmierung

214 7 Actors – Concurrency und Multicore-Programmierung

Echo1: 1: IntEcho1: 2: IntEcho1: 3: IntEcho1: 4: IntEcho1: 5: IntEcho1: 6: IntEcho1: 7: IntEcho1: 8: IntEcho1: 9: IntEcho1: 10: IntEcho2: 1: IntEcho2: 2: IntEcho2: 3: IntEcho2: 4: IntEcho1: ExitingEcho2: 5: IntEcho2: 6: IntEcho2: 7: IntEcho2: 8: IntEcho2: 9: IntEcho2: 10: IntEcho2: Exiting

Das Actors-Modell wird auch im Akka-Framework genutzt, das wir Ihnen in Ka-pitel 11 vorstellen werden.

Page 228: Scala : objektfunktionale Programmierung

Kapitel 8

Softwarequalität –Dokumentieren und Testen

Guter Code alleine reicht nicht. Auch wenn sich Programmiersprachen wie Scalagut dazu eignen, verständlich zu programmieren, sollte der Quellcode sorgfältigdokumentiert werden. Und zwar nicht nur, wenn es sich um Software handelt,die weitergegeben werden soll. Sorgfältig dokumentiert heißt, nicht zu viel undnicht zu wenig. Insbesondere das Application Programming Interface (API), alsoalles, was von außerhalb genutzt werden kann, sollte so beschrieben sein, dass eseinfach und ohne großen Aufwand verstanden werden kann.Qualitativ hochwertige Software hat möglichst keine Fehler. Dies zu beweisen, istin vielen Bereichen nicht möglich, mindestens aber zu aufwendig. Obwohl es sehrgute Ansätze im Bereich der Formalen Methoden1 gibt, finden diese nur seltenihren Weg in den normalen Programmieralltag.Auf jeden Fall aber muss die Software getestet werden. Im Kleinen heißt das fürviele Programmierer „ein wenig herumprobieren”. Das ist sicher hilfreich, dochbesser ist es aber, Testfälle zu spezifizieren und diese automatisiert ausführen zulassen. Dies sollte grundsätzlich mit der Programmierung einhergehen. Ein Test-Driven-Development, bei dem zuerst die Tests und dann die Software geschriebenwird, zählt in diesem Bereich zur „hohen Schule”.Im vorliegenden Kapitel beschäftigen wir uns mit der API-Dokumentation unddem Testen. Dabei liegt der Fokus darauf, wie in Scala dokumentiert wird undwie die zur Verfügung stehenden Testframeworks verwendet werden. In Ab-schnitt 8.1 geht es zunächst um die Verwendung von Scaladoc. Die folgendendrei Abschnitte stellen die wesentlichen Scala-Testframeworks vor, die übrigensalle auch für das Testen von Java-Code verwendet werden können. ScalaCheck(siehe Abschnitt 8.2) ist ein Framework mit dem Testdaten automatisch generiert

1 siehe auch [MH03]

Page 229: Scala : objektfunktionale Programmierung

216 8 Softwarequalität – Dokumentieren und Testen

werden können. Das Framework, mit dem die meisten verschiedenen Arten vonTests durchgeführt werden können, ist ScalaTest (siehe Abschnitt 8.3). Das Kapi-tel endet mit der Beschreibung des Specs-Framework, einer BDD2-Bibliothek (sie-he Abschnitt 8.4). Zum Vergleich der Testframeworks entwickeln wir alle mög-lichen Tests für eine sehr einfache Software zur Verwaltung von Videotheken.Diese sind als Sbt-Projekt unter http://github.com/obcode/moviestore_testframeworks verfügbar.

8.1 Scaladoc

Das Scaladoc-Tool haben wir bereits in Abschnitt 2.1.3 vorgestellt. Nun wollenwir Ihnen noch etwas näherbringen, wie Sie im Quellcode dokumentieren kön-nen. Kommentare, die mit Scaladoc automatisch in die API-Dokumentation auf-genommen werden sollen, müssen zwei Bedingungen erfüllen:

Scaladoc-Kommentare müssen mit /** beginnen und mit */ enden. Sie kön-nen sich über mehrere Zeilen erstrecken. Üblicherweise beginnen wir die neu-en Zeilen mit *.

Scaladoc-Kommentare können nur richtig zugeordnet werden, wenn sie ansinnvollen Stellen stehen:

– vor einer Klasse, einem Trait oder einer Objektdefinition (class, trait,object)

– vor einem Package-Objekt, nicht vor einem Package (package object)

– vor einer Member-Definition (def, val, var)

– vor Typ-Member-Definition (type)

Annotations und Modifier müssen zwischen den Scaladoc-Kommentar und dasjeweilige Schlüsselwort platziert werden.Scaladoc unterstützt die folgende Wiki-Syntax:

Inline Elemente

– kursive Schrift: ’’text’’ wird zu text.

– fette Schrift: ’’’text’’’ wird zu text.

– Unterstreichung: __text__ wird zu text.

– Monospace: ‘text‘ wird zu text.

– hochgestellter Text: ^text^ wird zu text.

– tiefgestellter Text: ,,text,, wird zu text.

– Entity Links: [[scala.collection.Set]] wird ein Link auf das Entity.

2 Behaviour-driven Development

Page 230: Scala : objektfunktionale Programmierung

8.1 Scaladoc 217

– externe Links: [[http://obraun.net/ Oliver Brauns Website]]wird zu einem Link, der angezeigt wird als „Oliver Brauns Website”.

Block-Elemente

– Absätze: Beginnen bzw. enden mit Leerzeile

– Quellcode: Beginnt mit {{{ und endet mit }}}

– Überschriften: Durch = vor und nach dem Text. Ein = entspricht der erstenEbene, zwei = der zweiten etc.

– Aufzählungen: Sequenz von gleichartigen Aufzählungselementen ohne Un-terbrechung

– Ungeordnete Aufzählungselemente: Beginnt mit mindestens einem Leerzei-chen und -. Solange gleich viele führende Leerzeichen verwendet werden,befinden wir uns auf einem Level. Mehr Leerzeichen bedeuten ein Sublevel.

– Geordnete Aufzählungselemente: Wie ungeordnete mit 1.,I., i., A. odera. vor allen Elementen, beispielsweise wird

/*** 1. erstes* 1. zweites*/

zu

1. erstes

2. zweites

Die einzelnen Sterne am linken Rand werden ignoriert. Das heißt, eine Zeiledie nur einen Stern und Leerzeichen enthält, wird als Leerzeile gewertet, undbei Quellcode werden die Sterne nicht mit ausgegeben.

HTML-Elemente werden nur noch zur Abwärtskompatibilität unterstützt undsollten nicht mehr verwendet werden. Frühere Versionen von Scaladoc unter-stützten noch keine Wiki-Syntax.Scaladoc unterstützt verschiedene sogenannte Tags. Ein Scaladoc-Kommentar istdabei so aufgebaut, dass zuerst ein beschreibender Teil kommen kann und danacheine Reihe von Tags. Ein Tag beginnt mit einem @-Zeichen, gefolgt von einemBezeichner. Je nach Tag hat das erste nachfolgende Wort eine spezielle Bedeutung.Der Inhalt des Tags endet mit dem Kommentar oder dem Beginn des nächstenTags. Die folgenden Tags werden von der aktuellen Scaladoc-Version unterstützt:

Parameter-Tags

– @param <name> <body>: Beschreibt den Parameter mit dem Bezeichner<name>.

Page 231: Scala : objektfunktionale Programmierung

218 8 Softwarequalität – Dokumentieren und Testen

– @tparam <name> <body>: Beschreibt den Typ-Parameter mit dem Be-zeichner <name>.

– @return <body>: Beschreibt, was zurückgegeben wird (nur für Methodenerlaubt).

– @throws <name> <body>: Beschreibt eine Exception vom Typ <name>,die von der Methode oder dem Konstruktor geworfen wird.

Usage-Tags

– @see <body>: Verweist auf eine andere Stelle für mehr Informationen.

– @note <body>: Beschreibt Vor- und Nachbedingungen oder Einschrän-kungen bzw. Anforderungen.

– @example <body>: Beispielnutzung und dazugehörige Beschreibung.

Inventory-Tags

– @author <author>: Der Autor. Bei mehreren Autoren für jeden einen ei-genen Tag.

– @version <version>: Version des kommentierten Artefakts.

– @since <version>: Seit welcher Version enthalten.

– @deprecated <body>: Markiert das Artefakt als veraltet. In der aktuellenScaladoc-Version wird es durchgestrichen.

Sonstige Tags

– @todo <body>: Dokumentiert noch Unimplementiertes.

Wird ein Member von einer Klasse oder einem Trait geerbt, wird auch derScaladoc-Kommentar übernommen. Einzelne Bereiche, die freie Beschreibungoder Tags können überschrieben werden.Darüber hinaus ist es noch möglich, Makros zu definieren. Mit

@define <name> <body>

wird ein Makro mit dem Namen <name> definiert. Damit werden dann alleVorkommen von $<name> in Kommentaren der dazugehörigen Einheit3 durch<body> ersetzt. Makros werden auch vererbt und können überschrieben werden.

Listing 8.1: Eine mit Scaladoc-Kommentaren versehene Klasse

/**** This class is ’’documented’’.** 1. first about this class

3 Wenn das Makro z.B. im Kommentar der Klasse definiert wurde, dann in allen Kommentaren derKlasse.

Page 232: Scala : objektfunktionale Programmierung

8.1 Scaladoc 219

* - sublevel* - also* 1. second about this class* 1. third** $ob* @version 1.0** @define ob @author Oliver Braun [[mailto:[email protected]

" <[email protected]>]]* @define DC [[org.obraun.scala.DocumentedClass]]*/class DocumentedClass(val id: Int) extends

DocumentedTrait {

DocumentedClass.ids += id

/*** The ID-Type.*/type ID = Int

/*** Another constructor without any params.*/def this() = this(0)

/*** Create new $DC object with the sum of* ‘this.id‘ and ‘that.id‘ as id.** @todo something to do* @param that another* $DC object** @return new DocumentedClass-object*/def +(that: DocumentedClass) = {val newID = this.id + that.idif (DocumentedClass ids newID)throw DocumentedClassObjectExistsException

new DocumentedClass(this.id+that.id)}def abstractMethod[T <: AnyVal](x: T) = id/*** @deprecated will be removed in 5 years*/val x = 7/*** The ‘toString‘-Method** {{{* println(DocumentedClass(19))

Page 233: Scala : objektfunktionale Programmierung

220 8 Softwarequalität – Dokumentieren und Testen

* }}}**/override def toString = "DocumentedClass-Object#"+id

}

In Listing 8.1 ist eine Beispieldokumentation für eine Klasse DocumentedClassdargestellt, die verschiedene Features von Scaladoc nutzt. Die generierte Doku-mentation ist in Abbildung 8.1 zu sehen.

Abbildung 8.1: API-Dokumentation der Klasse DocumentedClass

Page 234: Scala : objektfunktionale Programmierung

8.2 ScalaCheck 221

Eine gute Quelle, um mehr über das Aussehen und die Nutzung von Scaladoc zulernen, sind die Quellen und API-Dokumentationsseiten der Scala-Distribution.Im Folgenden werden wir auf die verschiedenen Testing-Frameworks, beginnendmit ScalaCheck, eingehen.

8.2 ScalaCheck

Das erste Werkzeug zum Automatisieren von Unit Tests, das wir Ihnen vorstellenwollen, ist ScalaCheck4. ScalaCheck entstand als Portierung von QuickCheck5, hataber mittlerweile auch Features, die in QuickCheck nicht vorhanden sind.

8.2.1 Grundlagen

Die Kernidee ist das Überprüfen von Eigenschaften (properties) mithilfe automa-tisch generierter Testdaten. Die Property ist die Testeinheit in ScalaCheck. ZumErzeugen einer Property beginnen wir mit der Methode org.scalacheck.-Prop.forAll, die eine Funktion als Parameter hat und eine Property repräsen-tiert, die für alle Eingabewerte gelten soll. Zum Beispiel können wir das Kommu-tativgesetz für die Addition zweier ganzer Zahlen wie folgt spezifizieren:

forAll { (a: Int, b: Int) => a + b == b + a }

Mathematisch würden wir das nur in anderer Syntax schreiben. Um so ein Pro-perty nun einfach in der Scala-Shell zu überprüfen, müssen wir als Erstes dieScalaCheck-Jar-Datei von http://code.google.com/p/scalacheck/ her-unterladen. Anschließend starten wir die Scala-Shell mit

$ scala -cp scalacheck-version.jar

wobei version als Platzhalter für die gerade aktuelle Version gedacht ist. Nunkönnen wir die Property definieren und checken:

scala> import org.scalacheck.Prop.forAllimport org.scalacheck.Prop.forAll

scala> val propCommutativePlus = forAll {| (a: Int, b: Int) => a + b == b + a| }

propCommutative: org.scalacheck.Prop = Prop

scala> propCommutativePlus.check+ OK, passed 100 tests.

4 http://code.google.com/p/scalacheck/5 Eine Haskell-Library zum Testen:http://www.haskell.org/haskellwiki/Introduction_to_QuickCheck

Page 235: Scala : objektfunktionale Programmierung

222 8 Softwarequalität – Dokumentieren und Testen

Nachdem wir die Property definiert haben, können wir sie mit der Methodecheck überprüfen. Wie wir in der Ausgabe sehen, hat ScalaCheck 100 Tests er-folgreich durchgeführt. Das heißt, es wurden für 100 Fälle Zufallsdaten generiertund für diese die Eigenschaft überprüft. Dies ist natürlich kein Beweis! Es könntedurchaus sein, dass es nicht betrachtete Fälle gibt, für die die Property doch nichtgilt. Umgekehrt können wir aber beweisen, dass die Property nicht gilt, sobald einTest fehlschlägt. Um dies zu demonstrieren, behaupten wir, dass eine Liste gleichbleibt, wenn wir sie mit reverse umdrehen:

scala> val propReverse = forAll {| (l: List[AnyVal]) => l.reverse == l| }

propReverse: org.scalacheck.Prop = Prop

scala> propReverse.check! Falsified after 2 passed tests.> ARG_0: List("-100", "true")

Wir sehen, dass die Property nicht hält. Interessanterweise hat ScalaCheck dieserst beim zweiten Test feststellen können. Die erste generierte Liste hat die Be-hauptung noch nicht widerlegt. ScalaCheck zeigt auch an, mit welcher Liste dieProperty falsifiziert wurde: List(-100, true).Manchmal ist es notwendig oder wünschenswert, den Testbereich einzuschrän-ken. Betrachten wir die folgende Property:

forAll { (a: BigInt, b: BigInt) => a * b / b == a }

So macht dies natürlich nur für b != 0 Sinn. Mithilfe des Implikations-Operators==> kann dies formuliert werden:

forAll { (a: BigInt, b: BigInt) =>b!=0 ==> (a * b / b == a)

}

Wenn die Bedingung nicht oder nur sehr schwierig zu erfüllen ist, kann die Pro-perty nicht getestet werden. ScalaCheck stoppt dann, wie in der folgenden Sit-zung zu sehen ist:

scala> import org.scalacheck.Prop._import org.scalacheck.Prop._

scala> val propTrivial = forAll {| (a: Int, b: Int) => a == b ==> (b == a)| }

propTrivial: org.scalacheck.Prop = Prop

scala> propTrivial.check! Gave up after only 5 passed tests. 500 tests were

discarded.

Page 236: Scala : objektfunktionale Programmierung

8.2 ScalaCheck 223

Properties können mit &&, ||, ==, all und atLeastOne kombiniert werden. Fürzwei Properties p und q gilt:

p && q gilt, wenn p und q gelten.

p || q gilt, wenn p oder q gelten.

p == q gilt, wenn p genau dort gilt, wo q gilt.

all(p,q) entspricht p && q.

atLeastOne(p,q) entspricht p || q.

Insbesondere beim Kombinieren von mehreren Properties zu einer wird die Infor-mation interessant, was genau dazu geführt hat, dass der Test fehlschlug. Dazukönnen Properties unter Verwendung einer der beiden Methoden |: und :| miteinem Label versehen werden. Dieses wird dann bei der Ausgabe angezeigt. DasLabel steht auf der Seite des | und die Property auf der Seite des :.

Listing 8.2: Eine Property, die mit Labels versehen ist

import org.scalacheck.Prop._val labelledProp = forAll { (a: String, b: String) =>val res = a + b("String = "+res |: all ("length" |: res.length == a.length + b.length,"reverse" |: res.reverse == b.reverse + a.reverse,"head" |: a.length>0 ==> (res.head == a.head),"headWrong" |: b.length>0 ==> (res.head == b.head))

)}

In Listing 8.2 ist eine Property mit Labels dargestellt. Testen wir die Property, se-hen wir Folgendes6 in der Scala-Shell:

scala> labelledProp.check! Falsified after 0 passed tests.> Labels of failing property:headWrongString = HelloWorld> ARG_0: Hello> ARG_1: World

Um mehrere Properties auf einmal zu testen, bietet sich die Klasse Propertiesan. Die Klasse selbst ist auch wieder eine Property, die dann gilt, wenn alle darinenthaltenen Properties gelten. In Listing 8.3 ist ein Beispiel angegeben.

6 Die in der Ausgabe verwendeten Strings Hello und World haben wir selbst eingefügt. Aufgrundvon Unicode waren bei zahlreichen Versuchen in der Scala-Shell immer nur asiatische Zeichen zusehen :-).

Page 237: Scala : objektfunktionale Programmierung

224 8 Softwarequalität – Dokumentieren und Testen

Listing 8.3: Eine ScalaCheck-Spezifikation

import org.scalacheck._

object MyScalaCheckSpec extends Properties("Int-Tests") {

import Prop.forAll

property("Commutativity Plus") = forAll {(i: Int, j: Int) => i+j == j+i

}

property("Commutativity Mult") = forAll {(i: Int, j: Int) => i*j == j*i

}

property("Associativity Plus") = forAll {(i: Int, j: Int, k: Int) => (i+j)+k == i+(j+k)

}

property("Associativity Mult") = forAll {(i: Int, j: Int, k: Int) => (i*j)*k == i*(j*k)

}

property("Distributivity") = forAll {(i: Int, j: Int, k: Int) => i*(j+k) == (i*j)+(i*k)

}

}

Das Objekt MyScalaCheckSpec können wir dann einfach übersetzen und aus-führen:

$ fsc -cp scalacheck-version.jar MyScalaCheckSpec$ scala -cp scalacheck-version.jar:. MyScalaCheckSpec+ Int-Tests.Commutativity Plus: OK, passed 100 tests.+ Int-Tests.Commutativity Mult: OK, passed 100 tests.+ Int-Tests.Associativity Plus: OK, passed 100 tests.+ Int-Tests.Associativity Mult: OK, passed 100 tests.+ Int-Tests.Distributivity: OK, passed 100 tests.

Mit der Methode include kann ein Properties-Objekt in ein anderes eingefügtwerden. Damit lassen sich Testfälle zunächst systematisch zuordnen und späterdurch Verwendung von include als ein einziges Properties-Objekt testen.

8.2.2 Generatoren

Ein wesentlicher Teil des ScalaCheck-Frameworks sind die Generatoren für dieErzeugung der zufälligen Werte, die in den Tests verwendet werden. Für diebisher in den Properties genutzten Datentypen sind die Generatoren bereits Be-standteil von ScalaCheck. Um neue Generatoren zu implementieren, gibt es ei-

Page 238: Scala : objektfunktionale Programmierung

8.2 ScalaCheck 225

nige Grundfunktionen und Kombinatoren. Mit der Funktion choose des org.-scalacheck.Gen-Objekts können zufällige Werte aus einem Int-, Long- oderDouble-Intervall generiert werden. Mithilfe von for-Komprehension könnenmehrere dann zu einem neuen Wert kombiniert werden. Beispielsweise könnenmit dem folgenden tupleGen-Generator Tupel generiert werden, deren ersteKomponenten zwischen 10 und 20 liegt. Die zweite Komponente liegt dann zwi-schen dem Doppelten und dem Dreifachen der ersten Komponente. Mit der Me-thode sample können wir ein Tupel erzeugen:

scala> import org.scalacheck.Gen._import org.scalacheck.Gen._

scala> val tupleGen = for {| a <- choose(10,20)| b <- choose(2*a,3*a)| } yield (a,b)

tupleGen: org.scalacheck.Gen[(Int, Int)] = Gen()

scala> tupleGen.sampleres0: Option[(Int, Int)] = Some((17,37))

Wie zu sehen ist, wird das Tupel als Option zurückgegeben. Dies gilt für alle Ge-neratoren. Neben choose gibt es noch oneOf und frequency. Mit beiden wirdaus vorgegebenen Werten ein zufälliger zurückgegeben. Bei oneOf geschieht dasfür alle mit gleicher Wahrscheinlichkeit, bei frequency kann die relative Häufig-keit für jeden Wert als Int angegeben werden. Mit

val stringGen1 = oneOf("Hello","World","and","Reader")

wird bei jedem Zugriff auf den Generator einer der vier Strings zurückgegeben,und zwar im Mittel jeder gleich oft. Mit

val stringGen2 = frequency((4,"Hello"),(2,"World"),(1,"and"),(3,"Reader")

)

wird Hello im Mittel viermal so oft wie and generiert. Auch für Case-Klassenkönnen wir sehr einfach einen eigenen Generator definieren. In Listing 5.24 aufSeite 122 haben wir zur abstrakten Klasse Lecture die drei Case-Klassen:

case class Course(title: String) extends Lecturecase class Exercise(belongsTo: Course) extends Lecturecase class Tutorial(belongsTo: Course) extends Lecture

definiert. In Listing 8.4 ist ein Generator dafür implementiert.

Page 239: Scala : objektfunktionale Programmierung

226 8 Softwarequalität – Dokumentieren und Testen

Listing 8.4: Ein Generator für Lectures

val genCourse = for {title <- oneOf("Prog 1","Prog 2","FP","OOP")

} yield Course(title)

val genExercise = for {course <- genCourse

} yield Exercise(course)

val genTutorial = for {course <- genCourse

} yield Tutorial(course)

val genLecture = oneOf(genCourse,genExercise,genTutorial)

Wir können unseren genLecture-Generator in der Scala-Shell ausprobieren:

scala> genLecture.sampleres1: Option[Product with Lecture] = Some(Course(Prog 2))

scala> genLecture.sampleres2: Option[Product with Lecture] = Some(Exercise(Course

(Prog 2)))

scala> genLecture.sampleres3: Option[Product with Lecture] = Some(Exercise(Course

(FP)))

Es ist auch möglich, Bedingungen für die generierten Werte mit der suchThat-Methode anzugeben. Beispielsweise generiert:

val smallEvenIntGen = choose(0,200) suchThat (_ % 2 == 0)

nur gerade Ints zwischen 0 und 200. Mit containerOf können standardmäßigfür verschiedene Container Werte generiert werden, z.B.

scala> val intListGen = containerOf[List,Int](genInt)intListGen: org.scalacheck.Gen[List[Int]] = Gen()

scala> intListGen.sampleres4: Option[List[Int]] = Some(List(235412, 18767, 0, ...

Für einige Container bietet ScalaCheck bereits Unterstützung. Durch Defini-tion einer impliziten Umwandlung in ein Buildable können weitere hin-zugefügt werden. Mit containerOf1 können nicht-leere Container und mitcontainerOfN Container mit einer vorgegebenen Größe erzeugt werden.Ein spezieller Generator ist arbitrary aus dem Objekt org.scalacheck.-Arbitrary. Dieser generiert beliebige Werte für alle unterstützten Typen. EinBlick in die API-Dokumentation zeigt, welche Typen dies sind. Beispielsweisekönnen wir mit:

Page 240: Scala : objektfunktionale Programmierung

8.2 ScalaCheck 227

import org.scalacheck.Arbitrary._val evenIntGen = arbitrary[Int] suchThat (_ % 2 == 0)

beliebige gerade Ints generieren. Eine Liste von Tupeln bestehend aus einemBigInt und seinem Quadrat bekommen wir zum Beispiel mit:

val intList = for {list <- arbitrary[List[BigInt]]

} yield list map (x => (x,x*x))

Für einen eigenen Typ T kann arbitrary genutzt werden, wenn eine impliziteUmwandlung vorhanden ist, die Arbitrary[T] als Ergebnistyp hat, also einView.

Listing 8.5: Implizite Umwandlung von Lecture in Arbitrary[Lecture]

implicit def arbLecture: Arbitrary[Lecture] =

Arbitrary{

val genCourse = for {title <- oneOf("Prog 1","Prog 2","FP","OOP")

} yield Course(title)

val genExercise = for {course <- genCourse

} yield Exercise(course)

val genTutorial = for {course <- genCourse

} yield Tutorial(course)

oneOf(genCourse,genExercise,genTutorial)

}

In Listing 8.5 ist eine solche Funktion für die Lectures durch Nutzung derArbitrary-Factory-Methode definiert. Anschließend können wir mit arbitra-ry eine Liste von Lectures generieren, wie die folgende Sitzung zeigt:

scala> val listOfLecturesGen = arbitrary[List[Lecture]]listOfLecturesGen: org.scalacheck.Gen[List[Lecture]] =

Gen()

scala> listOfLecturesGen.sampleres5: Option[List[Lecture]] = Some(List(Exercise(Course(

Prog 1)), Tutorial(Course(OOP)), Tutorial(...

Mit der Methode classify aus org.scalacheck.Prop schließlich können In-formationen über die generierten Werte gesammelt und angegeben werden.

Page 241: Scala : objektfunktionale Programmierung

228 8 Softwarequalität – Dokumentieren und Testen

Listing 8.6: Property mit Verwendung von classify

def ordered(l: List[Int]) = l == l.sort(_ > _)val listRevRev = forAll { l: List[Int] =>classify(ordered(l), "ordered") {classify(l.length > 5, "large", "small") {l.reverse.reverse == l

}}

}

In Listing 8.6 ist eine Funktion ordered, die überprüft, ob eine Liste sortiert ist,und ein Property listRevRev angegeben. Für jede generierte Liste wird über-prüft, ob sie sortiert ist und ob sie groß oder klein ist. Eine Beispielsitzung ergibtdann die folgende Ausgabe:

scala> listRevRev.check+ OK, passed 100 tests.> Collected test data:81% large11% small, ordered8% small

Soll nur gesammelt und ausgewertet werden, wie viel Prozent von jedem Wertgeneriert wurden, kann die Methode collect genutzt werden. Die folgende Sit-zung dient der Veranschaulichung:

scala> val lectureProp = forAll { l: Lecture =>| collect(l) {| l == l| }| }

lectureProp: org.scalacheck.Prop = Prop

scala> lectureProp.check+ OK, passed 100 tests.> Collected test data:12% Course(FP)12% Exercise(Course(Prog 2))11% Tutorial(Course(Prog 1))11% Course(Prog 1)11% Course(Prog 2)10% Course(OOP)9% Exercise(Course(OOP))8% Tutorial(Course(FP))6% Tutorial(Course(OOP))5% Exercise(Course(FP))4% Tutorial(Course(Prog 2))1% Exercise(Course(Prog 1))

Page 242: Scala : objektfunktionale Programmierung

8.2 ScalaCheck 229

8.2.3 Automatisiertes Testen mit Sbt

Zum Abschluss des Abschnitts über ScalaCheck wollen wir an einem Beispielerklären, wie ScalaCheck mit Sbt genutzt werden kann. Zum direkten Vergleichwerden wir in den folgenden Kapiteln dasselbe Beispiel mit Sbt und dem jeweili-gen Testframework verwenden.Als Beispiel soll eine einfache Videotheksverwaltung dienen, die in Listing 8.7dargestellt ist. Zunächst erzeugen wir ein Sbt-Projekt, wie in Abschnitt 2.2.2besprochen wurde. Unter src/main/scala/ erzeugen wir die Datei Movie-Store.scala mit, dem in Listing 8.7 dargestellten Inhalt. An dieser St,elle ver-zichten wir darauf den Code zu erläutern, und geben Ihnen die Chance sich imLesen und Verstehen von Scala-Code zu üben.

Listing 8.7: Die Klassen Movie und MovieStore

case class Movie(title: String, filmrating: Int)

class MovieStore {

private[this] var available = Map[Int,Movie]()private[this] var rent = Map[Int,Movie]()

def addToStore(movie: Movie) {MovieStore.serial += 1available += (MovieStore.serial -> movie)

}

def addToStore(movies: Traversable[Movie]) {movies foreach (addToStore(_))

}

def rentMovie(serial: Int): Option[Movie] = {val movieOption = available get serialmovieOption match {case None => Nonecase Some(movie) =>available -= serialrent += (serial -> movie)movieOption

}}

def returnMovie(serial: Int) = {val movie = rent(serial)rent -= serialavailable += (serial -> movie)

}

def availableMoviesForAge(age: Int) =available.filter{

Page 243: Scala : objektfunktionale Programmierung

230 8 Softwarequalität – Dokumentieren und Testen

case (_,Movie(_,r)) => r <= age}

def availableMovies = available

def rentMovies = rent

}object MovieStore {private var serial = 0

}

Die Datei src/test/scala/MovieStoreScalaCheck.scala mit den Testsist in Listing 8.8 dargestellt. Wir haben dort zwei Arbitrary-Instanzen und sechsProperties definiert.

Listing 8.8: Das Objekt MovieStoreScalaCheck zum Testen mit ScalaCheck

import org.scalacheck._

object MovieStoreScalaCheckextends Properties("MovieStore") {

import Gen._import Arbitrary._

implicit def arbMovie: Arbitrary[Movie] =Arbitrary( for{

title <- arbitrary[String]filmrating <- oneOf(6,12,16,18)

} yield Movie(title,filmrating))

implicit def arbSetMovie: Arbitrary[Set[Movie]] =Arbitrary( for {

pairs <- arbitrary[Set[(String,Int)]]} yield pairs map {case (t,f) => Movie(t,f)}

)

import Prop._

property("Add") = forAll { m: Movie =>val movieStore = new MovieStoremovieStore addToStore mmovieStore.availableMovies.size == 1movieStore.rentMovies.size == 0

}

property("AddAndRent") = forAll { m: Movie =>val movieStore = new MovieStoremovieStore addToStore mval keys = movieStore.availableMovies.keysmovieStore rentMovie keys.head

Page 244: Scala : objektfunktionale Programmierung

8.2 ScalaCheck 231

movieStore.availableMovies.size == 0movieStore.rentMovies.size == 1

}

property("AddAndTryRent") = forAll { m: Movie =>val movieStore = new MovieStoremovieStore addToStore mval keys = movieStore.availableMovies.keysmovieStore rentMovie (keys.head + 1)movieStore.availableMovies.size == 1movieStore.rentMovies.size == 0

}

property("AddAddRentReturn") = forAll {(m: Movie,n: Movie) =>val movieStore = new MovieStoremovieStore addToStore mmovieStore addToStore nval keys = movieStore.availableMovies.keysval serial = keys.headval movieOption = movieStore rentMovie serialmovieOption match {case None => throw new NoSuchElementExceptioncase Some(movie) => movieStore returnMovie serial

}movieStore.availableMovies.size == 2movieStore.rentMovies.size == 0

}

property("AddSet") = forAll {(ms: Set[Movie]) =>val movieStore = new MovieStoremovieStore addToStore msval setOfMovies: Set[Movie] =(movieStore.availableMovies :\ Set[Movie]()){case ((_,v),s) => s + v

}setOfMovies == ms

}

property("AddSetAge") = forAll {(ms: Set[Movie], age: Int) =>val movieStore = new MovieStoremovieStore addToStore msval setOfMoviesForAge: Set[Movie] =(movieStore.availableMoviesForAge(age) :\

Set[Movie]()){case ((_,v),s) => s + v

}setOfMoviesForAge ==

ms.filter{case Movie(_,f) => f <= age}}

}

Page 245: Scala : objektfunktionale Programmierung

232 8 Softwarequalität – Dokumentieren und Testen

Wenn wir nun noch das heruntergeladene ScalaCheck-JAR in das Verzeichnis libkopieren, können wir in der Sbt-Konsole das Kommando test eingeben und se-hen Folgendes:

> test...[info] == MovieStoreScalaCheck ==[info] + MovieStore.Add: OK, passed 100 tests.[info] + MovieStore.AddAndRent: OK, passed 100 tests.[info] + MovieStore.AddAndTryRent: OK, passed 100 tests.[info] + MovieStore.AddAddRentReturn: OK, passed 100

tests.[info] + MovieStore.AddSet: OK, passed 100 tests.[info] + MovieStore.AddSetAge: OK, passed 100 tests.[info] == MovieStoreScalaCheck ==...[success] Successful.

Sbt führt die Tests im Testverzeichnis automatisch aus. Und auch noch einbisschen mehr lässt sich mit Sbt automatisieren: Es ist nicht notwendig, dasScalaCheck-Jar selbst herunterzuladen und nach lib zu kopieren. Stattdessenkönnen wir das Sbt-Projekt so konfigurieren, dass das Jar mit einem sbt updateautomatisch heruntergeladen und nach lib_managed kopiert wird. Dazu erzeu-gen wir in der Datei project/build/MovieStore.scala eine Konfigurationfür das Projekt, wie in Listing 8.9 angegeben.

Listing 8.9: Eine Projekt-Definition, die das ScalaCheck-Jar managt

import sbt._

class MovieStoreProject(info: ProjectInfo) extendsDefaultProject(info) {

val scalacheck = "org.scalacheck" % "scalacheck" %"1.7" % "test" from"http://scalacheck.googlecode.com/files/scalacheck_

2.8.0-1.7.jar"

}

8.3 ScalaTest

Das zweite Test-Framework, das wir Ihnen vorstellen wollen, ist ScalaTest. UmScalaTest zu nutzen, müssen wir zunächst das aktuelle Jar von http://www.scalatest.org/ herunterladen. ScalaTest unterstützt verschiedene Arten vonTests, die wir in den folgenden Abschnitten kurz vorstellen wollen. Eine Einfüh-

Page 246: Scala : objektfunktionale Programmierung

8.3 ScalaTest 233

rung in die verwendeten zusätzlichen Frameworks wie JUnit7 oder TestNG8 wür-de den Rahmen dieses Buches bei Weitem sprengen. Da ScalaTest selbst sehr um-fangreich ist, werden wir auch hier nur Beispiele zeigen können. Dafür erklärenwir aber bei allen Arten unmittelbar das Testen der Klasse MovieStore (sieheListing 8.7) unter der Verwendung von Sbt.

8.3.1 ScalaTest und JUnit

JUnit ist ein Java-Unit-Test-Framework. Auch wenn JUnit in den Versionen 3 und4 von ScalaTest unterstützt wird, beschränken wir uns hier auf die Zusammenar-beit mit JUnit 4. Für eine Einführung in JUnit besuchen Sie bitte die JUnit-Websiteunter http://www.junit.org/. Außerdem können Sie dort das aktuelle JUnit-Jar herunterladen.ScalaTest bringt im Package org.scalatest.junit die notwendige Unterstüt-zung für JUnit mit. Wenn wir den darin enthaltenen Trait JUnitSuite hinein-mixen, stehen uns neben den ScalaTest-Assertions auch die JUnit-Assertions zurVerfügung. In Listing 8.10 ist ein Beispiel für eine Test-Suite dargestellt.

Listing 8.10: ScalaTest und JUnit

import org.scalatest.junit.JUnitSuite

class MovieStoreScalaTestJUnit extends JUnitSuite {

import org.junit.Assert._import org.junit.Test

val m: Movie = Movie("Am Limit",6)

@Test def add() {val movieStore = new MovieStoremovieStore addToStore massertEquals(movieStore.availableMovies.size,1)assertEquals(movieStore.rentMovies.size,0)

}

@Test def addAndRent() {val movieStore = new MovieStoremovieStore addToStore mval keys = movieStore.availableMovies.keysmovieStore rentMovie keys.headassert(movieStore.availableMovies.size == 0)assert(movieStore.rentMovies.size == 1)

}

@Test def exceptionTest() {

7 siehe auch [TLMG10] und http://junit.sourceforge.net/8 siehe auch [BS07] und http://testng.org/

Page 247: Scala : objektfunktionale Programmierung

234 8 Softwarequalität – Dokumentieren und Testen

val movieStore = new MovieStoreintercept[NoSuchElementException] {movieStore.availableMovies(12)

}}

}

Die mit der Annotation @Test versehenen Methoden entsprechen den Tests. Zu-sätzlich könnten auch noch die Annotationen @Before und @After für Initiali-sierungen vor bzw. „Aufräumarbeiten” nach den Tests verwendet werden.Die Test-Methode add nutzt JUnit-Assertions (assertEquals), die MethodeaddAndRent Scala-Test-Assertions (assert). Im Gegensatz zu ScalaCheck müs-sen wir die Testdaten selbst definieren. Jeder Test wird dann genau einmal durch-geführt.Die Methode exceptionTest nutzt die intercept-Methode von ScalaTest.Dieser Test ist genau dann erfolgreich, wenn eine NoSuchElementExceptiongeworfen wird.Um die Test-Suite mit Sbt auszuführen, muss die Klasse aus Listing 8.10 im Test-verzeichnis des Sbt-Projekts liegen, und das ScalaTest- sowie JUnit-Jar müssennach lib/ kopiert werden. Natürlich können wir die Projektkonfiguration ausListing 8.9 auch so erweitern, dass die beiden gemanagt werden. Dazu ergänzenwir einfach die beiden folgenden Definitionen:

val junit = "org.junit" % "junit" % "4.8.2" % "test" from"http://github.com/KentBeck/junit/downloads/junit

-4.8.2.jar"val scalatest = "org.scalatest" % "scalatest" % "1.2" % "

test"

Nachdem das Jar für ScalaTest in einem Repository liegt, das Sbt kennt, müssenwir dafür keine Quelle angeben. Dann führen wir wieder sbt update aus undsehen nach sbt test folgende Ausgabe:

...[info] == MovieStoreScalaTestJUnit ==[info] Test Starting: add[info] Test Passed: add[info] Test Starting: addAndRent[info] Test Passed: addAndRent[info] Test Starting: exceptionTest[info] Test Passed: exceptionTest[info] == MovieStoreScalaTestJUnit ==...[success] Successful.

Page 248: Scala : objektfunktionale Programmierung

8.3 ScalaTest 235

8.3.2 ScalaTest und TestNG

TestNG ist ein weiteres Test-Framework für Java, das zusammen mit ScalaTest ge-nutzt werden kann. Nachdem das benötigte TestNG-Jar mit seinen Abhängigkei-ten im Maven-Repository liegt, welches Sbt kennt, erweitern wir als Erstes unsereProjektkonfiguration um folgenden val:

val testng = "org.testng" % "testng" % "5.13" % "test"

und laden die Jars mit sbt update herunter. Die JUnit-Testsuite ist in wenigenSchritten in eine TestNG-Testsuite umgebaut. Gegenüber Listing 8.10 müssen wirnur die Importe und den hineingemixten Trait ändern. In Listing 8.11 ist das Re-sultat dargestellt.

Listing 8.11: ScalaTest und TestNG

import org.scalatest.testng.TestNGSuite

class MovieStoreScalaTestTestNG extends TestNGSuite {

import org.testng.Assert._import org.testng.annotations.Test

val m: Movie = Movie("Am Limit",6)

@Test def add() {val movieStore = new MovieStoremovieStore addToStore massertEquals(movieStore.availableMovies.size,1)assertEquals(movieStore.rentMovies.size,0)

}

@Test def addAndRent() {val movieStore = new MovieStoremovieStore addToStore mval keys = movieStore.availableMovies.keysmovieStore rentMovie keys.headassert(movieStore.availableMovies.size == 0)assert(movieStore.rentMovies.size == 1)

}

@Test def exceptionTest() {val movieStore = new MovieStoreintercept[NoSuchElementException] {movieStore.availableMovies(12)

}}

}

Führen wir die Tests aus, so kommt folgende Ausgabe für die TestNG-Suite dazu:

...

Page 249: Scala : objektfunktionale Programmierung

236 8 Softwarequalität – Dokumentieren und Testen

[info] == MovieStoreScalaTestTestNG ==[TestNG] Running:Command line suite

[info] initialize[info] Suite Starting: MovieStoreScalaTestTestNG[info] Test Starting: add[info] Test Passed: add[info] Test Starting: addAndRent[info] Test Passed: addAndRent[info] Test Starting: exceptionTest[info] Test Passed: exceptionTest[info] Suite Completed: MovieStoreScalaTestTestNG

===============================================Command line suiteTotal tests run: 3, Failures: 0, Skips: 0===============================================

[info] == MovieStoreScalaTestTestNG ==...

8.3.3 ScalaTest und BDD

BDD steht für Behavior-driven Development und stellt als solches die textuelle Be-schreibung des Programmverhaltens in den Vordergrund. Natürlich muss diesesVerhalten dann durch einen geeigneten Test verifiziert werden.ScalaTest bietet drei Traits, die sich nur in der Art und Weise unterscheiden, wieder Spezifikationstext zu formulieren ist: Spec, WordSpec und FlatSpec. Lis-ting 8.12 zeigt unsere Spezifikation unter Verwendung des WordSpec-Traits.

Listing 8.12: ScalaTest mit dem WordSpec-Trait

import org.scalatest.WordSpecimport org.scalatest.matchers.ShouldMatchers

class MovieStoreScalaTestWordSpec extends WordSpec withShouldMatchers {

val m = Movie("Am Limit",6)

"A MovieStore" when {"added one movie" should {

val movieStore = new MovieStoremovieStore addToStore m

"have one movie in availableMovies" in {assert(movieStore.availableMovies.size == 1)

}

Page 250: Scala : objektfunktionale Programmierung

8.3 ScalaTest 237

"have no movie in rentMovies" in {assert(movieStore.rentMovies.size == 0)

}}

"added and rent one movie" should {

val movieStore = new MovieStoremovieStore addToStore mval keys = movieStore.availableMovies.keysmovieStore rentMovie keys.head

"have no movie in availableMovies" in {assert(movieStore.availableMovies.size == 0)

}"have one movie in rentMovies" in {assert(movieStore.rentMovies.size == 1)

}}

"empty" should {

val movieStore = new MovieStore

("throw NoSuchElementException"+" when lookup a serial") in {evaluating {movieStore.availableMovies(12)

} should produce [NoSuchElementException]}

}}

}

Die Zeilen mit den Texten und when, should bzw. in beschreiben das Verhal-ten. In den dazugehörigen Blöcken stehen die Tests. Ziel ist, das Testdokumentso gut lesbar wie möglich zu machen. Daher wird auch der Teil, der auf dieNoSuchElementException testet, flüssiger lesbar programmiert.Nachdem wir nur das ScalaTest-Jar benötigen, können wir die Tests ohne weitereKonfiguration von Sbt sofort ausführen. Zusätzlich zur bisherigen Ausgabe sehenwir die folgenden Zeilen:

...[info] == MovieStoreScalaTestWordSpec ==[info] A MovieStore (when added one movie)[info] Test Starting: A MovieStore (when added one movie)

should have one movie in availableMovies[info] Test Passed: A MovieStore (when added one movie)

should have one movie in availableMovies[info] Test Starting: A MovieStore (when added one movie)

should have no movie in rentMovies

Page 251: Scala : objektfunktionale Programmierung

238 8 Softwarequalität – Dokumentieren und Testen

[info] Test Passed: A MovieStore (when added one movie)should have no movie in rentMovies

[info] A MovieStore (when added and rent one movie)[info] Test Starting: A MovieStore (when added and rent

one movie) should have no movie in availableMovies[info] Test Passed: A MovieStore (when added and rent one

movie) should have no movie in availableMovies[info] Test Starting: A MovieStore (when added and rent

one movie) should have one movie in rentMovies[info] Test Passed: A MovieStore (when added and rent one

movie) should have one movie in rentMovies[info] A MovieStore (when empty)[info] Test Starting: A MovieStore (when empty) should

should throw NoSuchElementException when lookup aserial

[info] Test Passed: A MovieStore (when empty) shouldshould throw NoSuchElementException when lookup aserial

[info] == MovieStoreScalaTestWordSpec ==...

Bei der Verwendung von Spec statt WordSpec müssen wir <description>when und <description> should durch describe(<description>) so-wie <description> in durch it(<description>) ersetzen. Die Verwen-dung von FlatSpec erlaubt keine Verschachtelung der Texte.

8.3.4 Funktionale, Integrations- und Akzeptanztests

Mit FeatureSpec können Szenarien von Features beschrieben und getestet wer-den. Dabei steht noch mehr als bei den vorgestellten BDD-Ansätzen die textuelleBeschreibung im Vordergrund. Listing 8.13 zeigt ein Beispiel für die Verwendungdes FeatureSpec-Traits.

Listing 8.13: ScalaTest mit dem FeatureSpec-Trait

import org.scalatest.FeatureSpecimport org.scalatest.GivenWhenThenimport org.scalatest.matchers.MustMatchers

class MovieStoreScalaTestFeatureSpec extends FeatureSpecwith GivenWhenThen with MustMatchers {

feature("The user can add a movie to the moviestore") {

scenario("added one movie to an empty moviestore") {given("an empty moviestore")val movieStore = new MovieStoreand("a movie")val m = Movie("Am Limit",6)when("added the movie to the moviestore")

Page 252: Scala : objektfunktionale Programmierung

8.3 ScalaTest 239

movieStore addToStore mthen("availableMovies contains one movie")movieStore.availableMovies.size must be === 1and("rentMovies contains no movie")movieStore.rentMovies.size must be === 0

}

scenario("added one movie to an empty moviestore"+" and rent it") {

given("an empty moviestore")val movieStore = new MovieStoreand("a movie")val m = Movie("Am Limit",6)when("added the movie to the moviestore")movieStore addToStore mand("rent it")val keys = movieStore.availableMovies.keysmovieStore rentMovie keys.headthen("availableMovies contains no movie")movieStore.availableMovies.size must be === 0and("rentMovies contains one movie")movieStore.rentMovies.size must be === 1

}scenario("added no movie, but try to get one from"+

" availableMovies") {given("an empty moviestore")val movieStore = new MovieStorewhen("trying to get a movie from availableMovies")then("NoSuchElementException should be thrown")evaluating {movieStore.availableMovies(12)

} must produce [NoSuchElementException]}

}}

Neben dem FeatureSpec-Trait wurde noch der GivenWhenThen- und derMustMatchers-Trait hineingemixt. Damit lassen sich nun Szenarien von Fea-tures sehr ausführlich beschreiben. Alle Texte werden auch in der Ausgabe desTestlaufs wiedergegeben, die wie folgt aussieht:

...[info] == MovieStoreScalaTestFeatureSpec ==[info] Feature: The user can add a movie to the

moviestore[info] Test Starting: Feature: The user can add a movie

to the moviestore added one movie to an empty moviesto[info] Test Passed: Feature: The user can add a movie to

the moviestore added one movie to an empty moviestore[info] Given an empty moviestore[info] And a movie[info] When added the movie to the moviestore

Page 253: Scala : objektfunktionale Programmierung

240 8 Softwarequalität – Dokumentieren und Testen

[info] Then availableMovies contains one movie[info] And rentMovies contains no movie[info] Test Starting: Feature: The user can add a movie

to the moviestore added one movie to an empty moviestoand rent it[info] Test Passed: Feature: The user can add a movie to

the moviestore added one movie to an empty moviestorend rent it[info] Given an empty moviestore[info] And a movie[info] When added the movie to the moviestore[info] And rent it[info] Then availableMovies contains no movie[info] And rentMovies contains one movie[info] Test Starting: Feature: The user can add a movie

to the moviestore added no movie, but try to get one fm availableMovies[info] Test Passed: Feature: The user can add a movie to

the moviestore added no movie, but try to get one froavailableMovies[info] Given an empty moviestore[info] When trying to get a movie from availableMovies[info] Then NoSuchElementException should be thrown[info] == MovieStoreScalaTestFeatureSpec ==...

8.3.5 Die FunSuite

Die letzte Möglichkeit, Tests in ScalaTest zu spezifizieren, ist die FunSuite, wobeiFun für Function steht. Ein Test ist eine Funktion mit dem Namen test, der alserste Parameterliste der Name und als zweite der auszuführende Code übergebenwird. Formulieren wir unsere Tests mit FunSuite, sehen sie wie in Listing 8.14aus.

Listing 8.14: ScalaTest mit dem FunSuite-Trait

import org.scalatest.FunSuiteimport org.scalatest.matchers.ShouldMatchers

class MovieStoreScalaTestFunSuite extends FunSuitewith ShouldMatchers {

val m: Movie = Movie("Am Limit",6)

test("add") {val movieStore = new MovieStoremovieStore addToStore mmovieStore.availableMovies.size should be === 1movieStore.rentMovies.size should be === 0

}

Page 254: Scala : objektfunktionale Programmierung

8.4 Specs 241

test("addAndRent") {val movieStore = new MovieStoremovieStore addToStore mval keys = movieStore.availableMovies.keysmovieStore rentMovie keys.headmovieStore.availableMovies.size should be === 0movieStore.rentMovies.size should be === 1

}

test("exceptionTest") {val movieStore = new MovieStoreevaluating {movieStore.availableMovies(12)

} should produce [NoSuchElementException]}

}

Diese Tests sehen nun wieder mehr nach Code und weniger nach Spezifika-tion aus. Wie zu erkennen ist, können wir die MustMatchers bzw. hier dieShouldMatchers unabhängig von der Testart in ScalaTest nutzen. Die Ausga-be unserer FunSuite sieht dann folgendermaßen aus:

...[info] == MovieStoreScalaTestFunSuite ==[info] Test Starting: add[info] Test Passed: add[info] Test Starting: addAndRent[info] Test Passed: addAndRent[info] Test Starting: exceptionTest[info] Test Passed: exceptionTest[info] == MovieStoreScalaTestFunSuite ==...

8.4 Specs

Das dritte Scala-Testing-Framework heißt Specs9 und ist eine BDD-Bibliothek.Auch in Specs läßt sich JUnit integrieren. Außerdem bietet Specs eine Integrati-on von ScalaCheck und verschiedene Mocking-Frameworks. Im Folgenden gebenwir Ihnen einen kleinen Überblick über Specs und seine Features und nutzen dazuden schrittweisen Aufbaus einer Spezifikation für den MovieStore aus Listing8.7 .

9 http://code.google.com/p/specs/

Page 255: Scala : objektfunktionale Programmierung

242 8 Softwarequalität – Dokumentieren und Testen

8.4.1 Eine Specs-Spezifikation

Die Grundidee des Testens mit Specs ist das Erstellen und Überprüfen einer Spe-zifikation. Dazu definieren wir ein Objekt, das die Klasse Specification erwei-tert, wie in Listing 8.15 dargestellt.

Listing 8.15: Ein Objekt, das die Klasse Specification erweitert

import org.specs._object MovieStoreSpecs extends Specification

Um gleich mit Sbt durchstarten zu können, erweitern wir die Projektdefinition mitder folgenden Zeile:

val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5" % "test"

Nach einem sbt update können wir den Specs-Test bereits ausführen. Die Aus-gabe von Sbt bezüglich Specs lautet dann:

...[info] == MovieStoreSpecs ==[info] totalSpecification[info] == MovieStoreSpecs ==...

Um die Spezifikation beim weiteren Entwickeln immer wieder übersetzen undausführen zu lassen, starten wir mit sbt die interaktive Sbt-Shell und geben~test ein. Damit wird bei jedem Speichern sofort das Sbt-Kommando test get-riggert, das die Tests ausführt und dazu vorher übersetzt.Nun aber wieder zurück zur Spezifikation. Wir geben der Spezifikation nocheinen Namen, den wir als Klassen-Parameter an Specification übergeben.Außerdem beschreiben wir schon einmal das zu testende System, den Movie-Store. Der aktuelle Stand unserer Spezifikation ist in Listing 8.16 zu sehen.

Listing 8.16: Die MovieStore-Spezifikation mit einem Namen und dem System

object MovieStoreSpecs extendsSpecification("MovieStore Specification") {

"A MovieStore" should {}}

Wir sehen nun die folgenden zusätzlichen Zeilen in der Sbt-Ausgabe:

[info] A MovieStore should[info] o PENDING: not yet implemented

Statt should kann auch can genommen werden, wenn es eine bessere Beschrei-bung ergibt. Außerdem können wir auch eigene Wörter definieren. Beispielsweisekönnen wir mit der Definition

Page 256: Scala : objektfunktionale Programmierung

8.4 Specs 243

def provide = addToSusVerb("provide")

auch spezifizieren:

"A MovieStore" should provide {}

Innerhalb des Systems beschreiben wir Beispiele10, unsere Tests, mit einemString und der Methode in oder alternativ >>. Diese Beispiele können wir auchineinander verschachteln. In Listing 8.17 ist das Gerüst für das erste Beispiel an-gegeben, wobei wir zwei verschiedene Eigenschaften überprüfen wollen.

Listing 8.17: Verschachtelte Beispiele in der Spezifikation

"A MovieStore" should {"when added one movie" >> {"contain one movie in availableMovies" >> {}"contain no movie in rentMovies" >> {}

}}

Speichern wir die Datei ab, sehen wir die zusätzlichen Beschreibungen in der Sbt-Ausgabe:

[info] A MovieStore should[info] o when added one movie[info] o contain one movie in availableMovies[info] o contain no movie in rentMovies

Specs unterstützt den Test-First-Ansatz dadurch, dass wir die Rümpfe der Bei-spiele leer lassen dürfen, wie in Listing 8.17 zu sehen ist. Es gibt aber auch dieMöglichkeit, die Implementierung des Tests bereits zu schreiben, aber den Testnoch nicht scharf zu schalten. Dazu gibt es einerseits die Möglichkeit, ein skip-Statement an den Anfang des Beispiels zu schreiben. Andererseits können wir denTrait PendingUntilFixed aus dem Package org.specs.specificationhineinmixen und dessen Methode pendingUntilFixed nutzen. Die Ausgabeändert sich dadurch nicht. In Listing 8.18 sind die Beispiele fertig programmiert,aber auf pending gesetzt.

Listing 8.18: Zwei Beispiele, die mit skip bzw. pendingUntilFixed auf pending gesetztsind

import org.specs._import org.specs.specification.PendingUntilFixed

object MovieStoreSpecs extends

10 Im Englischen werden die Tests Examples genannt. Wir übersetzen dies mit Beispiele.

Page 257: Scala : objektfunktionale Programmierung

244 8 Softwarequalität – Dokumentieren und Testen

Specification("MovieStore Specification") withPendingUntilFixed {

val movie = Movie("Step Across the Border",0)"A MovieStore" should {"when added one movie" >> {val movieStore = new MovieStoremovieStore addToStore movie"contain one movie in availableMovies" >> {pendingUntilFixed {movieStore.availableMovies.size must_== 1

}}"contain no movie in rentMovies" >> {skip("not yet implemented")movieStore.rentMovies.size must_== 0

}}

}}

Der Vorteil von pendingUntilFixed gegenüber skip ist, dass der Test bei Ver-wendung von pendingUntilFixed trotzdem ausgeführt und angezeigt wird,sobald er erfolgreich war:

...[info] A MovieStore should[info] x when added one movie[info] Fixed now. You should remove the ’pending

until fixed’ declaration (PendingUntilFixed.scala:68)[info] x contain one movie in availableMovies[info] Fixed now. You should remove the ’pending

until fixed’ declaration (PendingUntilFixed.scala:68)[info] o contain no movie in rentMovies...

Um dies nicht zu übersehen, schlägt der gesamte Test damit sogar fehl.

8.4.2 Matchers

In Listing 8.18 haben wir schon einen ersten Matcher gesehen: must_==. Für dieEigenschaften, die mit einem Test überprüft werden sollen, werden in Specs eineVielzahl solcher Matcher verwendet. Die grundsätzliche Verwendung eines Mat-chers ist:

<object> must <matcher>(<parameter>)

Ein Beispiel für einen solchen Ausdruck ist:

"hello" must beMatching("h.*")

Page 258: Scala : objektfunktionale Programmierung

8.4 Specs 245

Ein Teil der Matcher, die Specs mitbringt, beginnen mit be oder have. Bei die-sen können die Matcher in zwei Worten geschrieben werden. Beispielsweise kannstatt des oben angegebenen Ausdrucks folgender geschrieben werden:

"hello" must be matching("h.*")

Damit sieht die Spezifikation gleich lesbarer aus. Andere sehr lesbare Beispielesind:

"hello" must not be matching("z.*")List("hello") must not have size(2)Map("hello" -> "world") must have the key("hello")"hello" must be matching("h.*") andnot be matching("z.*")

Matcher sind Case-Klassen die von org.specs.matcher.Matcher abgeleitetsind und eine apply-Methode besitzen. Die apply-Methode muss einen By-Name-Parameter vom Typ des Objekts haben, das überprüft werden soll. Ergebnismuss ein Tripel sein, bestehend aus einem Wahrheitswert als Aussage, ob der Testerfolgreich war, und zwei Zeichenketten, die bei Erfolg bzw. Misserfolg ausgege-ben werden.Damit ist es auch möglich, eigene Matcher zu schreiben. In Listing 8.19 ist dieImplementierung des Matchers haveOnlyMoviesForTheAgeOf dargestellt.

Listing 8.19: Der haveOnlyMoviesForTheAgeOf-Matcher

import org.specs.matcher.Matchercase class haveOnlyMoviesForTheAgeOf(age: Int)

extends Matcher[Map[Int,Movie]] {def apply(movies: => Map[Int,Movie]) = {((movies filter {

case (_,(Movie(_,f))) => f > age}).isEmpty,"only allowed movies","not only allowed movies")

}}

Damit können wir in einem Beispiel die Bedingung mit folgendem Ausdrucküberprüfen:

movieStore.availableMoviesForAge(age) musthaveOnlyMoviesForTheAgeOf(age)

Es ist auch möglich, Matcher selbst zu definieren, die mit be oder have als Ex-trawort geschrieben werden können. Dazu müssen wir eine implizite Konversionvon Result[T] aus dem Package org.specs.specification zu einer Klas-se vornehmen, die eine Methode mit dem Namen ohne be bzw. have hat. In Lis-

Page 259: Scala : objektfunktionale Programmierung

246 8 Softwarequalität – Dokumentieren und Testen

ting 8.20 ist die implizite Konversion und die Klasse OnlyMoviesForTheAge-OfResult dargestellt.

Listing 8.20: Die Klasse OnlyMoviesForTheAgeOfResult und eine implizite Umwand-lung von Result[Map[Int,Movie]]

import org.specs.specification.Resultimplicit def toOnlyMoviesForTheAgeOfResult(

result: Result[Map[Int,Movie]])= new OnlyMoviesForTheAgeOfResult(result)

class OnlyMoviesForTheAgeOfResult(result: Result[Map[Int,Movie]]) {

def onlyMoviesForTheAgeOf(age: Int) =result.matchWithMatcher(

haveOnlyMoviesForTheAgeOf(age))}

Damit können wir die Bedingung nun folgendermaßen spezifizieren:

movieStore.availableMoviesForAge(age) musthave OnlyMoviesForTheAgeOf age

Matcher können mit and, or, xor, verifyAll und verifyAny zu neuen Mat-chern kombiniert werden. Mit when und unless kann die Anwendung von Mat-chern auf bestimmte Bedingungen angepasst werden. Außerdem gibt es nochEventually Matchers, die einen Matcher mehrfach anwenden und denen sogar eineWartezeit zwischen den Versuchen übergeben werden kann.Die Specs-Bibliothek definiert eine Vielzahl von Matchern, die für die verschie-denen Datentypen verwendet werden können. Daher können wir schließlich dieZeilen

movieStore.availableMovies.size must_== 1movieStore.rentMovies.size must_== 0

ersetzen durch:

movieStore.availableMovies must have size 1movieStore.rentMovies must have size 0

Die gesamte bis hierhin entwickelte Spezifikation zum Testen des MovieStoresmit dem Specs-Framework ist in Listing 8.21 dargestellt.

Listing 8.21: Die Specs-Spezifikation für den MovieStore

import org.specs._

object MovieStoreSpecs extendsSpecification("MovieStore Specification") {

val movie = Movie("Step Across the Border",0)"A MovieStore" should {

Page 260: Scala : objektfunktionale Programmierung

8.4 Specs 247

"when added one movie" >> {val movieStore = new MovieStoremovieStore addToStore movie"contain one movie in availableMovies" >> {movieStore.availableMovies must have size 1

}"contain no movie in rentMovies" >> {movieStore.rentMovies must have size 0

}}"when added a set of movies" >> {val movieStore = new MovieStoremovieStore addToStore Set(movie,Movie("Am Limit",6),Movie("The Matrix",16),Movie("Bad Taste",18),Movie("Bad Lieutenant",16)

)"return only movies which are allowed"+" for the given age" >> {import org.specs.matcher.Matchercase class haveOnlyMoviesForTheAgeOf(age: Int)

extends Matcher[Map[Int,Movie]] {def apply(movies: => Map[Int,Movie]) = {((movies filter {

case (_,(Movie(_,f))) => f > age}).isEmpty,"only allowed movies","not only allowed movies")

}}import org.specs.specification.Resultimplicit def toOnlyMoviesForTheAgeOfResult(

result: Result[Map[Int,Movie]])= new OnlyMoviesForTheAgeOfResult(result)

class OnlyMoviesForTheAgeOfResult(result: Result[Map[Int,Movie]]) {

def onlyMoviesForTheAgeOf(age: Int) =result.matchWithMatcher(

haveOnlyMoviesForTheAgeOf(age))}val age = 15movieStore.availableMoviesForAge(age) musthave onlyMoviesForTheAgeOf age

}}

}}

Page 261: Scala : objektfunktionale Programmierung

248 8 Softwarequalität – Dokumentieren und Testen

8.4.3 Mocks mit Mockito

Stellen wir uns einmal vor, wir würden eine komplexere Videothekensoftware ineinem Team entwickeln. Nach wie vor sind wir für die Klasse MovieStore ver-antwortlich. Zusätzlich gibt es aber ein Modul zur Kundenverwaltung, an demein anderes Teammitglied arbeitet. Für eine neue Fassung der Klasse Movie-Store wollen wir für die ausgeliehenen Filme die Kundennummer speichern.Genau genommen wollen wir statt der Map[Int,Movie] eine Map[Customer-ID, Map[Int,Movie]] verwenden. Damit wollen wir beispielsweise beim Lei-hen eines Films überprüfen, ob der Kunde bereits fünf Filme ausgeliehen hat.Einen sechsten Film soll er nämlich nicht bekommen. Außerdem soll in der Kun-denverwaltung die Möglichkeit implementiert sein, einen bestehenden Kundenzu deaktivieren. Dann darf er überhaupt keine Filme ausleihen.Um unsere angepasste Klasse zu testen, müssen wir nun auch die Klassen derKundenverwaltung implementieren. Dies bedeutet natürlich einen gewissen Auf-wand, bei dem sich auch Fehler einschleichen können, die das Testen dann schwermachen. Üblicherweise wird heute ein anderer Weg beschritten. Statt eine verein-barte Schnittstelle zu implementieren, wird sie gemockt (to mock = etwas vortäu-schen). Dazu gibt es eine Reihe von sogenannten Mocking-Bibliotheken, mit de-nen wir auf sehr einfache Weise ein Objekt erzeugen können, das sich so verhält,als würde es die Schnittstelle implementieren.Mockito11 ist eine solche Bibliothek. Specs bietet eine Anbindung an Mockito undnoch zwei weitere Mocking-Bibliotheken: EasyMock12 und JMock13. Außerdementhält Specs noch ein leichtgewichtiges eigenes Mocking Framework.Im Folgenden wollen wir einen Blick auf die Mockito-Anbindung werfen. Als Bei-spiel sollen die in Listing 8.22 dargestellten Traits dienen.

Listing 8.22: Die Traits Customer und CustomerID

trait CustomerID

trait Customer {val id: CustomerIDdef isDisabled: Boolean

}

In der MovieStore-Spezifikation nutzen wir Mockito, indem wir den TraitMockito aus org.specs.mock hineinmixen. Mit der Methode mock könnenwir dann Mock-Objekte erzeugen. Die folgenden Zeilen definieren ein Customer-und ein CostumerID-Mock-Objekt:

val customer = mock[Customer]val customerID = mock[CustomerID]

11 http://code.google.com/p/mockito/12 http://easymock.org/13 http://www.jmock.org/

Page 262: Scala : objektfunktionale Programmierung

8.4 Specs 249

Um die customerID mit dem customer in Zusammenhang zu bringen, schrei-ben wir einfach:

customer.id returns customerID

Wir können das in einem Beispiel mit:

customer.id mustEqual customerID

testen. Auch für isDisabled geben wir ein Ergebnis an:

customer.isDisabled returns false

In diesem Fall reicht diese einfache Definition. Für Methoden mit Parametern gibtes auch die Möglichkeit, mit answers eine Funktion zu definieren, die die Pa-rameter nutzt. Mocks haben sogar noch eine wesentlichen Vorteil gegenüber dertatsächlichen Implementierung: Wir können ganz einfach überprüfen, ob eine Me-thode aufgerufen wurde und sogar wie oft. Damit können wir zum Beispiel durchden Ausdruck

there was atLeastOne(customer).isDisabled

überprüfen, ob die Methode isDisabled in der rentMovie-Methode mindes-tens einmal aufgerufen wurde. In dem Fall können wir davon ausgehen, dassüberprüft wurde, ob der Kunde gesperrt ist.Um Mockito mit Sbt nutzen zu können, erweitern wir die Projektdefinition nochum Folgendes:

val mockito = "org.mockito" % "mockito-all" % "1.8.5" %"test"

8.4.4 Literate Specifications

Auch wenn sich Literate Specifications zur Zeit, da dieses Buch geschrieben wird,noch im Alpha-Stadium befinden, wollen wir sie nicht unerwähnt lassen. DieIdee dabei ist, Systeme als informellen Text vielleicht sogar mit Bildern zu spezi-fizieren. Teile des Textes sind Specs-Beispiele, die ausgeführt werden können. DieSpecs-Website enthält ein Beispiel für eine solche Spezifikation, die wir in Listing8.23 übernommen haben.

Listing 8.23: Eine Literate Specification (Quelle: Specs-Website)

class HelloWorldSpecification extends HtmlSpecificationwith Textile {

"The greeting application" is <t>

h3. Presentation

Page 263: Scala : objektfunktionale Programmierung

250 8 Softwarequalität – Dokumentieren und Testen

This new application should say "hello" in differentlanguages.

For example,<ex>by default, saying hello by defaultshould use English</ex>

{ greet must_== "hello" }

Then, other languages, like <ex>French and German shouldbe supported too</ex>

{ eg {greet("French") must_== "bonjour"greet("German") must_== "hallo"

}}

<ex>Japanese should be supported also</ex>{ notImplemented }

</t>}

Dazu einige Erläuterungen:

Der Name des Systems ist The greeting application.

Das System ist durch das XML-Element <t></t> spezifiziert.

Durch Verwendung des Textile-Traits kann die Überschrift mit h3. Pre-sentation erzeugt werden.

Die Namen der Specs-Beispiele stehen in <ex></ex>-Elementen.

Der Scala-Code für die Beispiele steht direkt dahinter in geschweiften Klam-mern.

Die eg-Funktion kapselt mehrere Erwartungen.

Ein weiteres Feature von Literate Specifications sind Data Tables. In Listing 8.24ist ein Beispiel dafür dargestellt.

Listing 8.24: Ein Beispiel für die Verwendung von Data Tables (Quelle: Specs-Website)

All those small examples should be ok: {"examples are ok" inTable"a" | "b" | "sum" |1 ! 1 ! 2 |1 ! 2 ! 3 |1 ! 3 ! 4 | { (a: Int, b: Int, sum: Int) =>a + b must be equalTo(sum)

}}

Page 264: Scala : objektfunktionale Programmierung

8.4 Specs 251

Mit diesem Ausblick auf neue Features von Specs wollen wir das Kapitel überDokumentation und Testing abschließen. Zusammenfassend bleibt festzuhalten:Testing ist wichtig, und im Scala-Umfeld gibt es einige Testing-Frameworks, dierichtig Spaß machen! In den folgenden Kapiteln werden wir uns nun mit einigenanderen Scala-Frameworks beschäftigen.

Page 265: Scala : objektfunktionale Programmierung
Page 266: Scala : objektfunktionale Programmierung

Kapitel 9

Webprogrammierung mit Lift

Lift1 ist ein Framework, mit dem Webapplikationen in Scala entwickelt werdenkönnen. Lift ist ein sogenanntes Full Stack Web Application Framework. Das heißt,Lift bringt alles Wesentliche mit, was wir für eine Webapplikation benötigenkönnten: eine Template-Engine, AJAX- und Comet-Unterstützung, eine Persis-tenzlösung, Link- und Zugriffsmanagement und ein komplettes User Manage-ment. Darüber hinaus gibt es eine Vielzahl weiterer nützlicher Module wie z.B.Textile-Unterstützung für die Nutzung von Wiki-Syntax in Eingabefeldern odereine PayPal-Integration.Einen Teil der Lift-Features werden wir Ihnen in diesem Kapitel anhand der Er-stellung einer Webapplikation vorstellen und Ihnen auf diesem Weg einen Ein-stieg in das Framework geben.Die Webapplikation, die wir erstellen wollen, heißt Talk Allocator. Sinn und Zweckder Anwendung soll es sein, Nutzern die Möglichkeit zu geben, sich aus einerMenge von Vortragsthemen ein passendes herauszusuchen, das sie bearbeitenund vortragen wollen. Das System kann dann beispielsweise an einer Hochschu-le zur Vergabe von Seminar-, Projekt- oder Referatsthemen an die Studierendenverwendet werden.Wir beginnen in Abschnitt 9.1 mit dem Einstieg in die Lift-Entwicklung undzeigen Ihnen, wie Sie den vom Lift-Team zur Verfügung gestellten Prototypenzum Laufen bekommen. In Abschnitt 9.2 beschäftigen wir uns mit dem Boot-strapping, also mit den Dingen, die beim Start der Lift-Applikation gemachtwerden sollen. Einen ersten Blick auf das Rendering mit Templates und Snip-pets werfen wir in Abschnitt 9.3. Lift hat eingebaute Unterstützung für die Be-nutzerverwaltung und die SiteMap (siehe Abschnitt 9.4). Außerdem bringt Lifteinen OR-Mapper mit, dessen Verwendung wir in Abschnitt 9.5 vorstellen wol-len. Das Kapitel schließt mit der Besprechung der Implementierung der Snip-

1 Siehe auch [CBWD09] und http://liftweb.net/.

Page 267: Scala : objektfunktionale Programmierung

254 9 Webprogrammierung mit Lift

pets in Abschnitt 9.6. Der in diesem Kapitel erstellte Code ist verfügbar unterhttp://github.com/obcode/talkallocator_lift.

9.1 Quickstart mit Lift

Das Entwickeln unserer Lift-Applikation müssen wir nicht mit einem leeren Ver-zeichnis beginnen. Das Lift-Team bietet die Möglichkeit, ein initiales, lauffähigesLift-Projekt für Maven bzw. Sbt zu erstellen. Wir verwenden im Folgenden Sbt, dadies in der Scala-Community mittlerweile die bevorzugte Variante ist.Wollen Sie dennoch lieber mit Maven arbeiten, sollten Sie der Einfachheit halbereine Version ab 2.2 verwenden. Diese enthält bereits einen Archetyp für Lift. Inder aktuell vorliegenden Maven-Version 2.2.1 sind das die Nummern:

250: remote -> liftweb-archetype-blank (Archetype - blankproject for liwftweb)

251: remote -> liftweb-archetype-hellolift (Archetype -hellolift sample liwftweb application)

Wir starten hier also mit Sbt. Das initiale Projekt müssen wir dazu aber erst ein-mal herunterladen. Dazu nutzen wir einen Prototyp, der über den Lift-GitHub-Account2 bereitgestellt wird. Der von uns im Folgenden verwendete Prototypnennt sich lift_21_sbt und nutzt Scala 2.8.0 und Lift 2.1. Wir verwenden auchGit3 als Versionskontrollsystem und können damit den Prototyp einfach klonen:

$ git clone git://github.com/lift/lift_21_sbt.gittalkallocator

Mit dem oben angegebenen Kommando klonen wir das Git-Repository unterdem Verzeichnisnamen talkallocator. Wenn Sie kein Git installiert haben,können Sie das Projekt auch von http://github.com/lift/lift_21_sbt/downloads als tgz- oder zip-Datei herunterladen. Ein Blick in das geklontebzw. entpackte Verzeichnis zeigt die in Listing 9.1 dargestellte Struktur.

Listing 9.1: Dateien und Verzeichnisse des Lift-Prototyps

+-- project| +-- build| | +-- LiftProject.scala| +-- build.properties+-- src

+-- main| +-- resources| | +-- props| | +-- default.props

2 http://github.com/lift3 http://git-scm.com/

Page 268: Scala : objektfunktionale Programmierung

9.1 Quickstart mit Lift 255

| +-- scala| | +-- bootstrap| | | ‘-- liftweb| | | ‘-- Boot.scala| | +-- code| | +-- comet| | +-- lib| | | +-- DepencyFactory.scala| | +-- model| | | +-- User.scala| | +-- snippet| | | +-- HelloWorld.scala| | +-- view| +-- webapp| +-- images| | +-- ajax-loader.gif| +-- index.html| +-- static| | +-- index.html| +-- templates-hidden| | +-- default.html| | +-- wizard-all.html| +-- WEB-INF| +-- web.xml+-- test

+-- resources+-- scala

+-- code| +-- AppTest.scala| +-- snippet| +-- HelloWorldTest.scala+-- LiftConsole.scala+-- RunWebApp.scala

Im Verzeichnis talkallocator starten wir Sbt. Nachdem Sbt die notwendi-gen Bibliotheken nicht ohne Aufforderung herunterlädt, geben wir das Kom-mando update ein. Um die lauffähige Webapplikation einmal ausprobieren zukönnen, geben wir unter Sbt jetty-run ein. Dieses Kommando startet deneingebetteten Jetty-Webserver4. Wenn Sie nun noch einen Browser öffnen undhttp://localhost:8080/ eingeben, sollten Sie die in Abbildung 9.1 gezeig-te Webseite sehen.Die Beispielapplikation beinhaltet ein Usermanagement. Sie können sich regis-trieren, anmelden und wieder abmelden. Falls auf Ihrem Rechner an Port 25 einSMTP-Server lauscht, können Sie bereits eine „Passwort vergessen”-E-Mail ver-senden. Das alles passiert mit relativ wenig Code. Kehren wir noch einmal zurück

4 http://www.eclipse.org/jetty/

Page 269: Scala : objektfunktionale Programmierung

256 9 Webprogrammierung mit Lift

Abbildung 9.1: Lift-Prototyp Webapplikation

zu den Dateien aus Listing 9.1. Das Verzeichnis project beinhaltet die Konfigu-ration für das Sbt-Projekt, und unter src/test befinden sich Tests. Die eigentli-che Lift-Applikation besteht also nur aus den Dateien im Verzeichnis src/main,die wir uns im Folgenden einzeln ansehen wollen.Die Datei resources/props/default.props ist im Prototyp leer. Sie kannProperties enthalten wie z.B.

db.driver=com.mysql.jdbc.Driver

Es ist auch möglich, eigene Properties zu definieren. Im Verzeichnis scala befin-den sich die Scala-Dateien, unter webapp alle sonstigen wie z.B. Bilder, statischeHTML-Seiten und Templates. Im Verzeichnis scala gibt es zwei Unterverzeich-nisse.Das Verzeichnis bootstrap enthält die Datei liftweb/Boot.scala, die beimStarten der Webapplikation genutzt wird. Diese Datei werden wir uns im Ab-schnitt 9.2 genauer ansehen bzw. an unsere Bedürfnisse anpassen. Wichtig ist,dass diese Datei die Klasse Boot im Package bootstrap.liftweb mit einerMethode boot definiert. Genau diese Methode wird vom Lift-Servlet beim Star-ten ausgeführt.Die drei Dateien im Verzeichnis code enthalten die notwendigen Scala-Klassenund -Objekte für die laufende Applikation. Diesen Bereich werden wir uns in Ab-schnitt 9.3 gemeinsam mit den Templates im Verzeichnis src/main/webapp vor-nehmen. Der Verzeichnisname code ist dabei willkürlich gewählt. Wir werden esspäter durch das Verzeichnis org ersetzen, da wir für unseren Code das Packageorg.obraun.talkallocator verwenden werden.

Page 270: Scala : objektfunktionale Programmierung

9.2 Bootstrapping 257

9.2 Bootstrapping

Als Erstes wollen wir die Klasse bootstrap.liftweb.Boot anpassen. Im We-sentlichen geht es um die boot-Methode, die beim Start der Webapplikation aus-geführt wird. Wie im Prototyp wollen wir auch eine Datenbank zur Speicherungder Nutzer, aber auch der Informationen verwenden. Der im Prototyp dazu ent-haltene Code ist in Listing 9.2 dargestellt.

Listing 9.2: Code des Lift-Prototyps für die Datenbank-Connection

if (!DB.jndiJdbcConnAvailable_?) {val vendor =new StandardDBVendor(Props.get("db.driver") openOr "org.h2.Driver",Props.get("db.url") openOr"jdbc:h2:lift_proto.db;AUTO_SERVER=TRUE",Props.get("db.user"), Props.get("db.password"))

LiftRules.unloadHooks.append(vendor.closeAllConnections_! _)

DB.defineConnectionManager(DefaultConnectionIdentifier, vendor)

}

Schemifier.schemify(true, Schemifier.infoF _, User)

An diesem Code können wir bereits die ersten Lift-Konzepte erklären. Zunächstwird mit der Methode jndiJdbcConnAvailable_? des DB-Objektes überprüft,ob wir vom JNDI5 eine JDBC-Connection bekommen können. Lift definiert ei-ne Vielzahl von Methoden, die das Suffix _? oder _! haben, um anzuzeigen, obnach etwas gefragt wird oder nicht. Nachdem wir uns um unsere Datenbankver-bindung selbst kümmern wollen, lassen wir die if-Anweisung weg. Das, wasinnerhalb des Blocks steht, werden wir aber zum Teil noch benötigen.Für die Datenbankverbindung benötigen wir ein StandardDBVendor-Objekt.Damit wollen wir eine H2-Datenbank6 verwalten. Im Prototyp-Code wird mitProps.get versucht, auf Properties zuzugreifen, die beispielsweise in der Da-tei resources/props/default.props stehen könnten. Props.get liefert ei-ne Property als Wert vom Typ Box[String] zurück. Eine Box ist ähnlich wieeine Option mit ein paar zusätzlichen Features. Ist die Box leer, gibt die Metho-de openOr ihr Argument zurück, sonst den Inhalt der Box. Nachdem wir kei-ne Properties nutzen wollen7, können wir für die ersten beiden Argumente des

5 Java Naming and Directory Interface6 http://www.h2database.com/7 Natürlich ist es sinnvoll, die Props.get-Statements im Code zu lassen. Damit können wir spä-ter ganz einfach auf ein anderes Datenbankmanagementsystem wechseln. Aus didaktischen Gründenentfernen wir sie aber hier.

Page 271: Scala : objektfunktionale Programmierung

258 9 Webprogrammierung mit Lift

StandardDBVendor-Konstruktors direkt die jeweiligen Strings angeben. Dasdritte und vierte Argument, der DB-User und das Passwort, wird auch nicht benö-tigt. Nachdem der Konstruktor aber einen Wert vom Typ Box[String] benötigt,übergeben wir einfach jeweils eine leere Box durch Angabe des Objektes Empty.Das nächste Konzept, das wir Ihnen vorstellen möchten, ist das LiftRules-Objekt. Dieses Objekt dient als Container für nahezu alles, was in Lift konfi-guriert werden kann. In der entsprechenden Code-Zeile in Listing 9.2 wird derunloadHooks-Collection eine Funktion zum Schließen aller Connections ange-hängt. Diese wird dann beim Herunterfahren der Lift-Applikation ausgeführt.Das behalten wir sinnvollerweise so bei. Auch die Zeile zum Definieren derDatenbank-Connection als ConnectionManager lassen wir unverändert.Mit dem Schemifier wird ein Teil des O/R-Mappers von Lift genutzt. Die Me-thode schemify stellt dabei sicher, dass die Datenbank das richtige Schemazum Speichern der User-Objekte hat. Das „richtige” Schema ist in der Klassecom.model.User definiert, die wir im nächsten Abschnitt behandeln werden.Nachdem wir auch die Vorträge in einer eigenen Tabelle speichern wollen, fügenwir einfach die Klasse Talk als weiteres Argument für schemify hinzu. Damitdies kompiliert werden kann, müssen wir natürlich erst einmal die Klasse Talkimplementieren. Hierbei bitten wir Sie aber noch um etwas Geduld bis Abschnitt9.5.Der Code, der im Talk Allocator die Zeilen aus Listing 9.2 ersetzt, ist in Listing 9.3zu sehen.

Listing 9.3: Code des Talk Allocators für die Datenbank-Connection

val vendor =new StandardDBVendor("org.h2.Driver","jdbc:h2:talkallocator.db;AUTO_SERVER=TRUE",Empty, Empty)

LiftRules.unloadHooks.append(vendor.closeAllConnections_! _)

DB.defineConnectionManager(DefaultConnectionIdentifier, vendor)

Schemifier.schemify(true, Schemifier.infoF _, User, Talk)

Die nächste Zeile im Prototyp ist:

LiftRules.addToPackages("code")

Damit wird dem Lift-Framework mitgeteilt, im Package code nach Klassen undObjekten für die sogenannten Snippets (siehe Abschnitt 9.3) zu suchen. Wir änderndie Zeile ab in

Page 272: Scala : objektfunktionale Programmierung

9.2 Bootstrapping 259

LiftRules.addToPackages("org.obraun.talkallocator")

da wir dieses Package für unseren Code nutzen wollen. Die Besprechung undAnpassung der folgenden Zeilen, die für die SiteMap notwendig sind, verschie-ben wir auf Abschnitt 9.4. Auch die letzten fünf Statements lassen wir zunächstso, wie sie sind, sodass sich der derzeitige Inhalt der Datei Boot.scala wie inListing 9.4 gezeigt darstellt.

Listing 9.4: Die zum Teil überarbeitete Datei Boot.scala

package bootstrap.liftweb

import net.liftweb._import util._

import common._import http._import sitemap._import Loc._import mapper._

// noch benötigt, bis umgebautimport code.model._

class Boot {def boot {val vendor =new StandardDBVendor("org.h2.Driver","jdbc:h2:talkallocator.db;AUTO_SERVER=TRUE",Empty, Empty)

LiftRules.unloadHooks.append(vendor.closeAllConnections_! _)

DB.defineConnectionManager(DefaultConnectionIdentifier, vendor)

Schemifier.schemify(true, Schemifier.infoF _,User, Talk)

LiftRules.addToPackages("org.obraun.talkallocator")

// noch nicht angepasstval entries = List(Menu.i("Home") / "index",Menu(Loc("Static", Link(List("static"), true,

"/static/index"),"Static Content"))) :::

User.sitemapLiftRules.setSiteMap(SiteMap(entries:_*))

Page 273: Scala : objektfunktionale Programmierung

260 9 Webprogrammierung mit Lift

LiftRules.ajaxStart =Full(() =>LiftRules.jsArtifacts.show("ajax-loader").cmd)

LiftRules.ajaxEnd =Full(() =>LiftRules.jsArtifacts.hide("ajax-loader").cmd)

LiftRules.early.append(_.setCharacterEncoding("UTF-8"))

LiftRules.loggedInTest = Full(() => User.loggedIn_?)S.addAround(DB.buildLoanWrapper)

}}

Die LiftRules für ajaxStart und ajaxStop definieren das Ein- und Aus-blenden eines Images. Mit dem darauf folgenden Statement wird der verwende-te Zeichensatz auf UTF-8 gesetzt. Der loggedInTest wird verwendet, um zuunterscheiden, ob ein Nutzer erfolgreich eingeloggt ist oder nicht, um z.B. unter-schiedliche Menüpunkte anzuzeigen.Schließlich kommt noch das Objekt S aus dem Package net.liftweb.http insSpiel. Dieses Objekt repräsentiert den aktuellen Status des HTTP-Request und -Response. Es kann beispielsweise für ein Cookie-Management oder für Locali-zation/Internationalization genutzt werden. Mit der Methode addAround kannein Wrapper um den gesamten Request gepackt werden. Im konkreten Fall ist derWrapper definiert durch die Methode buildLoanWrapper aus dem DB-Objekt,die aus dem gesamten Request eine Datenbanktransaktion macht.Starten wir die Webapplikation mit dem Sbt-Kommando jetty-run mit der ver-änderten Boot-Klasse, sieht die Startseite unter http://localhost:8080/ auswie in Abbildung 9.2 zu sehen.Wir sehen eine Fehlermeldung, die besagt, dass ein Snippet nicht gefunden wur-de. Der Rest der Applikation funktioniert aber nach wie vor. Das Snippet liegt imPackage code, wir haben aber das Package org.obraun.talkallocator fürdie Snippets vorgegeben. Mit Snippets und der Darstellung beschäftigen wir unsim folgenden Abschnitt.Für diejenigen, die ihren Code gerne in einem Package zusammen haben und keinextra Package bootstrap.liftweb verwenden möchten, gibt es diese Möglich-keit natürlich auch. Um mit der Klasse org.obraun.talkallocator.Boot zustarten, müssen wir nur wenige Dinge verändern:

1. Wir verschieben die Datei Boot.scala aus dem Verzeichnis bootstrap/-liftweb nach org/obraun/talkallocator relativ zu src und verändernden Package-Namen entsprechend.

2. Wir erweitern die Klasse Bootable.

3. Wir fügen in die Datei webapp/WEB-INF/web.xml ein Element init--param ein, indem wir den Bootloader auf die neue Klasse setzen (siehe Lis-ting 9.5).

Page 274: Scala : objektfunktionale Programmierung

9.2 Bootstrapping 261

Abbildung 9.2: Startseite des Prototyps mit teilweise angepasster Boot-Klasse

Listing 9.5: Die Datei webapp/WEB-INF/web.xmlmit dem init-param-Element für dieeigene Boot-Klasse

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-appPUBLIC "-//Sun Microsystems, Inc.//DTD Web Application

2.3//EN""http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app><filter><filter-name>LiftFilter</filter-name><display-name>Lift Filter</display-name><description>The Filter that intercepts lift calls

</description><filter-class>net.liftweb.http.LiftFilter

</filter-class><init-param><param-name>bootloader</param-name><param-value>org.obraun.talkallocator.Boot

</param-value></init-param>

</filter>

<filter-mapping><filter-name>LiftFilter</filter-name><url-pattern>/*</url-pattern>

</filter-mapping></web-app>

Page 275: Scala : objektfunktionale Programmierung

262 9 Webprogrammierung mit Lift

9.3 Rendering – Templates und Snippets

Das Rendering-Konzept von Lift, das auch im Prototyp zur Anwendung kommt,ist die Verwendung von Templates. Über Lift-spezifische Tags werden Scala-Funktionen, sogenannte Snippets, eingebunden. Daneben gibt es noch die Mög-lichkeit, ohne Templates nur mithilfe von Scala-Funktionen, sogenannten Views,zu rendern. Wir beschränken uns im Folgenden auf die Verwendung von Templa-tes und Snippets.Wird http://localhost:8080/ aufgerufen, wird das Template mit dem Na-men default.html aus dem Verzeichnis templates-hidden unter wepappgerendert und ausgeliefert8. Ein Blick in die Datei zeigt HTML mit einigen Be-sonderheiten. Was wir ganz einfach anpassen können, ist der Titel der Seite, da essich dabei um das übliche HTML-Tag title im head handelt. Wir ersetzen dasbestehende Tag also durch:

<title>Talk Allocator</title>

Nach dem Titel sehen wir schon zwei Lift-spezifische Tags:

<lift:CSS.blueprint /><lift:CSS.fancyType />

Dabei handelt es sich um die Einbindung zweier Snippets. Die Tags haben dieForm <lift:snippet_name />, wobei der snippet_name eine Methode ei-ner Klasse oder eines Objekts ist. In den oben angegebenen Snippets sind das dieMethoden blueprint und fancyType des CSS-Objekts des Lift-Frameworks.Die Bedeutung des Snippet-Tags ist, die entsprechende Methode auszuführen undden Tag durch das Ergebnis zu ersetzen. Das heißt natürlich, dass die Methodeneinen Wert vom Typ scala.xml.NodeSeq zurückgeben müssen.Im Body des Templates können wir noch die Überschrift von app zu TalkAllocator ändern. Etwas weiter unten in der Datei sehen wir noch zwei sol-cher Snippet-Tags:

<lift:Menu.builder /><lift:Msgs showAll="true"/>

Durch das erste Tag wird an dieser Stelle die Methode builder des ObjektsMenu ausgeführt. Diese rendert das Menü mit dem SiteMap-Inhalt. Im zwei-ten Snippet-Tag steht keine Methode. Wird keine Methode angegeben, wird dierender-Methode ausgeführt, das heißt also in diesem Fall die render-Methodedes Objekts Msgs. Diese gibt Meldungen des Lift-Frameworks wie Fehlermeldun-gen, Warnungen oder Benachrichtigungen aus. Über das Attribut showAll, dasin der render-Methode ausgelesen wird, kann angegeben werden, ob alle Mel-dungen ausgegeben werden sollen.Das letzte Lift-Tag in der Datei default.html ist:

8 Das Template ist nicht wirklich der Einstiegspunkt. Genaueres etwas weiter unten.

Page 276: Scala : objektfunktionale Programmierung

9.3 Rendering – Templates und Snippets 263

<lift:bind name="content" />

Das ist kein Snippet-Tag, sondern gehört zu einer eigenständigen Klasse von Tags:den Bind-Tags. Das Bind-Tag definiert einen Platz mit einem Namen, an dem et-was eingefügt werden kann. Und damit kommen wir zum echten Einstiegspunktin die Applikation. Natürlich, Sie haben sicher richtig vermutet, wird beim Auf-ruf von http://localhost:8080/ nicht die Datei default.html, sondernindex.html ausgeliefert. Die im Lift-Prototyp mitgelieferte Datei index.htmlim Verzeichnis webapp ist in Listing 9.6 wiedergegeben.

Listing 9.6: Die Datei index.html aus dem Lift-Prototyp

<lift:surround with="default" at="content"><h2>Welcome to your project!</h2><p><lift:helloWorld.howdy><span>Welcome to your Lift app at <b:time/></span>

</lift:helloWorld.howdy></p>

</lift:surround>

Was wir als oberstes XML-Element in der Datei index.html sehen, ist das Ge-genstück zum Bind-Tag aus der Datei default.html. Das Surround-Tag nimmtdas Template, das mit dem Attribut with spezifiziert ist, und ersetzt das Bind-Tag mit dem Namen content durch den eigenen Inhalt, also von <h2> bis zu</p>. Dieser Inhalt enthält ein Snippet-Tag, das die Methode howdy der KlasseHelloWorld aufruft. Diesmal ist der Start- und End-Tag voneinander getrennt,und es steht Folgendes dazwischen:

<span>Welcome to your Lift app at <b:time/></span>

Auf diese Weise können wir eine NodeSeq als Argument an ein Snippet über-geben. Das heißt, es gibt Snippets ohne und Snippets mit einer NodeSeq als Ar-gument. Das verwendete Argument ist gültiges XML, enthält aber auch wiedereine Besonderheit: das Tag <b:time/>. Dieses Tag wird in der howdy-Methodeersetzt. Werfen wir dazu einen Blick auf die Methode:

import net.liftweb.util.Helpers.binddef howdy(in: NodeSeq): NodeSeq =bind("b", in, "time" -> date.map(d => Text(d.toString)))

Durch Verwendung von bind des Objekts Helpers wird <b:time/> durcheinen Text-Knoten mit dem aktuellen Datum ersetzt. Die Methode bind erwar-tet folgende Parameter:

Einen Namespace, also das Präfix vor dem Doppelpunkt – in diesem Fall b.

Die NodeSeq, in der ersetzt werden soll – in diesem Fall in.

Page 277: Scala : objektfunktionale Programmierung

264 9 Webprogrammierung mit Lift

Ein oder mehrere Mappings von Elementnamen (time) zu dem ersetzendenElement (date.map(d => Text(d.toString)))).

Das Feld date ist vom Typ Box[Date]. Wichtig bei dem Feld ist, dass es als lazyval definiert wurde. Dies stellt sicher, dass die aktuelle Zeit erst beim Zugriffermittelt wird. Nachdem HelloWorld kein Objekt, sondern eine Klasse ist, wirdbei jedem Laden von index.html ein neues Objekt erzeugt.Für den Talk Allocator ändern wir die Datei index.html ab, wie es in Listing 9.7dargestellt wird.

Listing 9.7: Die Datei index.html des Talk Allocators

<lift:surround with="default" at="content"><h2>Willkommen beim Talk Allokator</h2><h4>Noch nicht vergebene Talks sind</h4><lift:Talks.available /><h4>Bereits vergeben sind</h4><lift:Talks.allocated />

</lift:surround>

Wir nutzen das Template default und geben die noch nicht und die bereits ver-gebenen Talks aus. Dazu haben wir zwei Snippet-Tags angegeben. Damit diesfunktioniert, müssen wir eine Klasse oder ein Objekt Talks mit den beiden Me-thoden available und allocated implementieren. Befinden muss sich dasGanze dann im Sub-Package snippet eines dem Lift-Framework im Rahmen desBootstrapping bekannt gemachten Packages. Nachdem wir in der Boot-Klasse dasPackage org.obraun.talkallocator zu den LiftRules hinzugefügt haben,speichern wir unsere Snippets in org.obraun.talkallocator.snippet. Be-vor wir dies tun, beschäftigen wir uns jedoch in den nächsten beiden Abschnit-ten erst einmal mit der mitgelieferten Benutzerverwaltung, der SiteMap und demLift-eigenen OR-Mapper9.

9.4 Benutzerverwaltung und SiteMap

Das Lift-Framework enthält eine Benutzerverwaltung, die wir bereits im Prototypgesehen haben. Lift bietet zur Verwaltung eines Benutzers den Trait MegaProto-User. Im Folgenden passen wir die Datei User aus dem Prototyp an unsere Be-dürfnisse an. Genau genommen nehmen wir nur das, was wir nicht benötigen,aus der Klasse User und dem Companion-Objekt heraus. Die von uns verwende-te Datei User.scala ist in Listing 9.8 wiedergegeben.

9 Objektrelationale Abbildung, zur Speicherung von Objekten in einer relationalen Datenbank.

Page 278: Scala : objektfunktionale Programmierung

9.4 Benutzerverwaltung und SiteMap 265

Listing 9.8: Die Klasse User mit Companion-Objekt für die Benutzerverwaltung

package org.obraun.talkallocatorpackage modelimport net.liftweb.mapper._import net.liftweb.common._

class User extends MegaProtoUser[User] {def getSingleton = User

}

object User extends User with MetaMegaProtoUser[User] {override def dbTableName = "users"override def screenWrap = Full(<lift:surround with="default" at="content"><lift:bind />

</lift:surround>)override def skipEmailValidation = true

}

Die Klasse User fügt das MegaProtoUser-Trait hinzu. Dieses Trait definierteinen User, der unter Verwendung des OR-Mappers gespeichert werden kann. Dieeinzige abstrakte Methode, die implementiert werden muss, ist getSingleton.Diese Methode muss ein Objekt, den Meta-Server für diese Klasse, zurückgeben.Dieser Meta-Server stellt die notwendigen Informationen für die Datenbank etc.zur Verfügung und ist in unserem Fall das direkt unter der Klasse definierte Com-panion-Objekt.Durch Hineinmixen des Traits MetaMegaProtoUser zum Objekt User habenwir die gesamte benötigte Funktionalität. Wir übernehmen auch hier einige Rede-finitionen aus dem Lift-Prototyp. Wir geben einen eigenen Tabellennamen für dieSpeicherung der Benutzer an. Mit der Methode screenWrap wird die Darstel-lung der Webseiten der Benutzerverwaltung spezifiziert, und schließlich lassenwir es zu, dass sich neue Benutzer ohne E-Mail-Validierung registrieren können.Als Nächstes kehren wir noch einmal zur Boot-Klasse zurück und definieren dieSiteMap. Die dazu notwendigen Zeilen sind in Listing 9.9 angegeben.

Listing 9.9: Erstellung einer SiteMap in der Klasse Boot

val ifLoggedIn = If(() => User.loggedIn_?,() => RedirectResponse("/index"))

val ifAdmin = If(() => User.superUser_?,() => RedirectResponse("/index"))

val entries = List(Menu.i("Home") / "index",Menu(Loc("Add", List("add"),

"Talk hinzufügen / löschen", ifAdmin)),

Page 279: Scala : objektfunktionale Programmierung

266 9 Webprogrammierung mit Lift

Menu(Loc("Choose", List("choose"),"Talk auswählen", ifLoggedIn))

) ::: User.sitemap

LiftRules.setSiteMap(SiteMap(entries:_*))

Wir wollen eine SiteMap, in der es eine Startseite gibt. Erst wenn der Benutzereingeloggt ist, soll auch eine Seite für das Auswählen eines Talks hinzukommen.Handelt es sich bei dem Benutzer um einen Admin, bekommt dieser zusätzlicheine Seite, um neue Talks anzulegen und Talks zu löschen. Außerdem soll dieStandard-SiteMap für die Benutzerverwaltung hinzugefügt werden.Um dies umzusetzen, definieren wir entries, eine Liste von Menüeinträgen.Der erste Menüeintrag ist die einfachste Möglichkeit, einen Eintrag anzulegen.Mit der Methode i des Menu-Objekts erzeugen wir einen Eintrag, der als Nameund Linktext das übergebene Argument bekommt. Dem Ergebnis fügen wir mitder /-Methode einen Pfad hinzu.Zum Erzeugen der beiden Einträge Add und Choose nutzen wir das Loc-Objekt.Dessen apply-Methode übergeben wir einen Namen, einen Link, einen Link-Textund einen LocParam. Ein Link wird als Liste von Verzeichnissen repräsentiert, al-so müssten wir für den Link admin/add List("admin","add") angeben. EinLocParam schließlich modifiziert einen Eintrag. In unserem Beispiel haben wirdie beiden LocParams, ifLoggedIn und ifAdmin, der Übersichtlichkeit hal-ber als vals definiert. Der in beiden Fällen verwendete LocParam ist ein Ob-jekt der Klasse If. Der erste Parameter ist ein Prädikat, also eine Funktion, dieeinen Boolean zurückgibt. Der zweite Parameter spezifiziert das Verhalten, dasausgeführt werden soll, wenn das Prädikat nicht zutrifft. Die so implementier-te Liste konkatenieren wir mit der User.sitemap. Schließlich nutzen wir dieElemente der Liste entries einzeln, deshalb als entries:_*, zur Erzeugungeines SiteMap-Objekts und übergeben dieses an die Methode setSiteMap desLiftRules-Objekt.

9.5 Persistenz

Als Nächstes implementieren wir eine Klasse Talk für die Talks, die in der Da-tenbank über den OR-Mapper persistiert werden sollen. Ein Talk soll dabei einFeld für einen Titel und ein Feld für den Vortragenden haben. Ohne OR-Mappingwürden wir die Klasse wie folgt definieren:

class Talk(val title: String, var speaker: User)

Aufgrund der Verwendung des OR-Mappers müssen wir dieses Grundgerüst einkleines bisschen aufblähen, wie in Listing 9.10 dargestellt ist.

Page 280: Scala : objektfunktionale Programmierung

9.5 Persistenz 267

Listing 9.10: Die Klasse Talk

class Talk extends LongKeyedMapper[Talk] with IdPK {def getSingleton = Talkobject title extends MappedString(this,100)object speaker extends MappedLongForeignKey(this, User)

}

Um den OR-Mapper zu nutzen, müssen wir einen Mapper-Trait in die KlasseTalk hineinmixen. Wir wählen den LongKeyedMapper-Trait, der einen Long-Wert als Primary-Key in der Datenbank verwendet. Nachdem wir den Primary-Key nicht selbst definieren wollen, fügen wir eine passende Implementierungdurch den IdPK-Trait hinzu. Damit bekommt jedes Talk-Objekt einen eindeu-tigen Schlüssel mit dem Bezeichner id.Wie schon in der Klasse User (siehe Listing 9.8) müssen wir die Methode get-Singleton definieren. Analog zu User verwenden wir auch hier das Compa-nion-Objekt. Die Felder müssen wir für den OR-Mapper etwas anders definieren,nämlich als Objekte. Der Typ der Spalte in der Datenbank ergibt sich aus dem je-weils verwendeten Trait. Den Titel wollen wir als String mit einer maximalenLänge von 100 Zeichen speichern. Dazu nutzen wir den MappedString-Trait.Der erste Parameter ist das Talk-Objekt, zu dem dieses Feld gehört, der zweiteParameter gibt die maximale Länge an. Um den Vortragenden zu verwalten, nut-zen wir einen MappedLongForeignKey, also einen Long-Wert der als Fremd-schlüssel genutzt wird. Als Argumente übergeben wir wieder das Objekt, demdieses Feld gehört, und das User-Objekt als Ziel für den Fremdschlüssel.Das noch benötigte Companion-Objekt Talk ist in Listing 9.11 dargestellt.

Listing 9.11: Das Objekt Talk

object Talk extends Talk with LongKeyedMetaMapper[Talk] {override def dbTableName = "talks"

}

Das Talk-Objekt erweitert die Klasse Talk und fügt den Trait LongKeyedMeta-Mapper hinzu. Analog zur Benutzerverwaltung haben wir damit die für den OR-Mapper benötigte Funktionalität und können diese nach Bedarf abändern. Wirgeben der Tabelle, in der die Talks gespeichert werden, den Namen talks.Damit können wir in unserer Webapplikation Talk-Objekte nutzen, die in der Da-tenbank gespeichert werden. Der Zugriff auf die Felder der Talk-Objekte ist dabeiganz normal möglich. Wenn wir beispielsweise ein Talk-Objekt mit dem Bezeich-ner myTalk haben, können wir mit myTalk.title auf den Titel zugreifen. DasErzeugen, Verändern und Speichern von Objekten funktioniert aber etwas andersals sonst üblich.Ein Talk-Objekt erzeugen wir mit der Methode create des Talk-Companion-Objekts. Einen Wert für ein Feld setzen wir über die apply-Methode des das Feld

Page 281: Scala : objektfunktionale Programmierung

268 9 Webprogrammierung mit Lift

repräsentierende Objekts. Durch den folgenden Ausdruck erzeugen wir beispiels-weise ein Talk-Objekt mit dem Titel „Neues von Scala”:

Talk.create.title("Neues von Scala")

Mit der Methode save speichern wir das Objekt in der Datenbank.Für das Suchen von Objekten in der Datenbank gibt es verschiedene Methodendes Companion-Objekts. Die Methode find beispielsweise bekommt als Parame-ter einen QueryParam und gibt eine Box zurück, die entweder leer ist oder dasgefundene Objekt enthält. In Listing 9.12 ist die Methode createExampleTalksdefiniert, die wir zum Objekt Talk hinzufügen. Der in der Methode verwendeteQueryParam wird vom Objekt By erzeugt und sucht nach dem Wert talk imFeld title.

Listing 9.12: Die Methode createExampleTalks aus dem Talk-Objekt

def createExampleTalks() = {List("Scala 2.8.0 - Was gibt’s Neues?","Scala - OSGi-Bundles from Outer (Java) Space"

).foreach{talk =>if (find(By(title,talk)).isEmpty)create.title(talk).save

}}

Durch Ergänzen der Zeile

Talk.createExampleTalks()

in der Boot-Klasse wird beim Start der Webapplikation überprüft, ob die beidenTalks bereits in der Datenbank vorhanden sind. Wenn nicht, werden sie neu an-gelegt. Das jeweils nicht gesetzte Feld speaker hat in der Datenbank den WertNULL. Der Wert NULL kann auch explizit mit dem Methodenaufruf myTalk.-speaker(Empty) gesetzt werden.Um beim Applikationsstart auch gleich zwei Beispielnutzer anzulegen, ergän-zen wir das Objekt User um die in Listing 9.13 angegebene Methode create-ExampleUsers und rufen diese in der Boot-Klasse auf.

Listing 9.13: Die Methode createExampleUsers aus dem User-Objekt

def createExampleUsers() {if (find(By(email, "[email protected]")).isEmpty) {create.email("[email protected]")

.firstName("Hugo")

.lastName("Admin")

.password("talkadmin")

.superUser(true)

Page 282: Scala : objektfunktionale Programmierung

9.6 Implementierung der Snippets 269

.validated(true)

.save}if (find(By(email, "[email protected]")).isEmpty) {create.email("[email protected]")

.firstName("Egon")

.lastName("User")

.password("talkuser")

.validated(true)

.save}

}

9.6 Implementierung der Snippets

Was nun noch zum fertigen Talk Allocator fehlt, sind die Snippets und die Tem-plates für das Auswählen und das Administrieren der Talks. Das Template für dieStartseite haben wir bereits in Listing 9.7 gezeigt. Die Startseite soll dann wie inAbbildung 9.3 gezeigt aussehen.

Abbildung 9.3: Startseite des Talk Allocators

Die beiden benötigten Snippets wollen wir nun als Erstes implementieren. Da-zu erzeugen wir eine Datei Talks.scala im Sub-Package snippet des Packa-ges org.obraun.talkallocator. Wir benötigen zwei Methoden, availableund allocated, die jeweils eine NodeSeq als Ergebnis haben. Bei den bereitsvergebenen Talks soll dahinter in Klammern der Sprecher mit Vor- und Zunameangegeben werden. Da das nur ein kleiner Unterschied ist, implementieren wireine Methode talksAsTable, die ein Flag als Parameter hat, das angibt, welcheTalks, freie oder vergebene, angezeigt werden sollen. Das Objekt Talks mit dendrei Methoden ist in Listing 9.14 angegeben.

Page 283: Scala : objektfunktionale Programmierung

270 9 Webprogrammierung mit Lift

Listing 9.14: Das Objekt Talks mit den ersten Snippets

package org.obraun.talkallocatorpackage snippet

import scala.xml._

import net.liftweb._import mapper._import util.Helpers._import common._import http.S._import http.SHtml._

import model._

object Talks {def available = talksAsTable(true)

def allocated = talksAsTable(false)

def talksAsTable(available: Boolean) = {

def speaker(speakerID: MappedLong[Talk]) = {val speaker = User.find(By(User.id,speakerID)

).getText(speaker.firstName+" "+speaker.lastName)

}

val talks = Talk.findAll(if (available) NullRef(Talk.speaker)else NotNullRef(Talk.speaker)

)

<table>{ talks.map{

talk =><tr><th>{talk.title}</th>{if (!available)

<th width="20%">({speaker(talk.speaker)})

</th>}

</tr>}

}</table>

}

}

Page 284: Scala : objektfunktionale Programmierung

9.6 Implementierung der Snippets 271

Die Methode talksAsTable besteht aus drei Teilen: einer Funktion zum Aus-geben des Sprechers, der Berechnung der anzuzeigenden Talks und der Formu-lierung des Ergebniswertes. Nachdem der Sprecher als Fremdschlüssel im Talkenthalten ist, übergeben wir den Wert an die Funktion speaker. In der Funk-tion speaker ermitteln wir das dazugehörige User-Objekt mit der MethodeUser.find. Nachdem find eine Box zurückgibt, packen wir das Objekt mit getaus. Anschließend erzeugen wir aus dem Vor- und Zunamen ein Objekt vom Typscala.xml.Text.Zum Ermitteln der Talks nutzen wir die Methode findAll. Freie Talks haben inder Datenbank in der Spalte speaker den Wert NULL stehen. Über den Query-Param NullRef können wir so alle freien Talks ermitteln. Umgekehrt finden wirmit NotNullRef alle bereits vergebenen Talks.Die NodeSeq, die talksAsTable schließlich zurückgibt, ist eine Tabelle. Ausjedem Talk wird eine eigene Zeile gemacht, die entweder nur den Titel oder Titelund Sprecher enthält.

Abbildung 9.4: Startseite des Talk Allocators mit einem vergebenen Talk

Nachdem sich Egon User für einen Talk entschieden hat, sieht die Startseite zumBeispiel wie in Abbildung 9.4 aus. Damit sich Egon User überhaupt einen Talkauswählen kann, müssen wir das Template und das Snippet dafür implementie-ren. Listing 9.15 zeigt das Template für die Auswahlseite. Aussehen soll die Seitedann wie in Abbildung 9.5.

Listing 9.15: Das Template choose.html für die Auswahl eines Talks

<lift:surround with="default" at="content"><h2>Talk auswählen</h2><lift:Talks.choose form="post" />

</lift:surround>

Wie in Listing 9.15 zu sehen ist, wählen wir dieselbe Struktur mit dem default-Template. Das Snippet-Tag hat dieses Mal ein Attribut form. Damit wird das Snip-pet zum Formular. Implementiert werden soll es als Methode choose im Talks-Objekt. Diese Methode soll Folgendes leisten:

Page 285: Scala : objektfunktionale Programmierung

272 9 Webprogrammierung mit Lift

Abbildung 9.5: Auswahlseite des Talk Allocators

1. Klickt der Benutzer auf den Button „Keinen Talk übernehmen’,’ wird sicher-gestellt, dass ihm kein Talk zugeordnet ist. Hatte er bereits einen Talk, wird beidiesem der Sprecher wieder auf NULL gesetzt.

2. Es werden alle verfügbaren Talks als Radio-Button-Liste angezeigt. Hat derBenutzer bereits einen Talk ausgewählt, steht dieser ganz oben und ist bereitsmarkiert. Ist noch keiner ausgewählt gewesen, wird auch keiner vormarkiert.Damit führt ein sofortiges Klicken auf den Button „Auswählen” zu keiner Än-derung.

3. Wählt der Benutzer einen Talk aus und bestätigt dies mit dem Button „Aus-wählen”, wird ihm der entsprechende Talk zugeordnet. Ein vorher gewählterwird wieder freigegeben.

Listing 9.16: Die Methode Talks.choose

def choose = {

val user = User.currentUser.open_!val chosen = Talk.findAll(By(Talk.speaker,user.id))val available = Talk.findAll(NullRef(Talk.speaker))var newTitle: Option[String] = None

def chooseTalk(maybeTitle: Option[String]) = {

val hasOld = !chosen.isEmpty

maybeTitle match {case None if hasOld =>chosen.head.speaker(Empty).save

case Some(title) =>Talk.find(By(Talk.title,title)) match {case Full(talk) =>

Page 286: Scala : objektfunktionale Programmierung

9.6 Implementierung der Snippets 273

if (hasOld) {val old = chosen.headif (old.title != talk.title) {old.speaker(Empty).savetalk.speaker(user.id).save

}} else talk.speaker(user.id).save

case _ => error("Talk "+ title+"not found")}

case _ =>}

redirectTo("/")}val talks = radio((chosen:::available).map{ _.title.toString },if (chosen.isEmpty)Empty

elseFull(chosen.head.title),

title => newTitle = Some(title)).toForm

val choose = submit("Auswählen",() => chooseTalk(newTitle)

)

val chooseNone = submit("Keinen Talk übernehmen",() => chooseTalk(None)

)

talks :+ choose :+ chooseNone}

Die Methode choose ist in Listing 9.16 wiedergegeben. Im ersten Teil werden ei-nige vals und eine var definiert. Mit User.currentUser bekommen wir deneingeloggten Benutzer in einer Box die wir mit open_! auspacken. Der Zugriffmit open_! wirft eine Exception, wenn kein Benutzer eingeloggt ist. Aufgrundunserer SiteMap ist die entsprechende Seite aber nur zu sehen, wenn ein Benut-zer eingeloggt ist. Anschließend berechnen wir die Liste der bereits ausgewähltenTalks und weisen sie chosen zu. Obwohl nur maximal ein Talk zugewiesen seinkann, wird in solchen Fällen in der funktionalen Programmierung gerne mit Lis-ten gearbeitet. Dies macht beispielsweise auch das Erzeugen des Radio-Buttonsweiter unten ein kleines bisschen knapper. Das Ermitteln der verfügbaren Talkskennen wir schon aus der talksAsTable-Methode. Für den eventuell ausge-wählten Titel nutzen wir eine lokale Variable, die wir mit None vorbelegen.Die oben beschriebene Logik der Methode lagern wir in die Funktion choose-Talk aus, die den Wert von newTitle übergeben bekommt. Handelt es sich da-

Page 287: Scala : objektfunktionale Programmierung

274 9 Webprogrammierung mit Lift

bei um None, wird der eventuell vorher bereits gewählte Talk zurückgesetzt, in-dem dem speaker-Objekt eine leere Box übergeben und das Talk-Objekt mitsave in die Datenbank geschrieben wird. Wurde ein Titel übergeben, wird derTalk ermittelt. Anschließend werden die verschiedenen Fälle abgearbeitet.Am Ende der Funktion chooseTalk wird auf die Startseite umgeleitet. Die dazugenutzte Methode redirectTo gehört zum S-Objekt, das den aktuellen Zustanddes HTTP-Request und -Response repräsentiert.Nach der Funktion chooseTalk müssen wir noch das Ergebnis, also die Node-Seq definieren. Eine Gruppe von Radio-Buttons können wir mit der Methoderadio des SHtml-Objekts erzeugen. Die drei Parameter sind:

1. Die verschiedenen Optionen, bei uns die verschiedenen Talks. Nachdem derWert von chosen eine Liste ist, können wir diese, egal ob leer oder nicht, ein-fach vor die Liste der verfügbaren Talks hängen.

2. Die vorausgewählte Option als Box. Dabei entspricht Empty keiner Voraus-wahl.

3. Die Funktion, die mit der gewählten Option ausgeführt wird. Wir weisen diegewählte Option verpackt in ein Some der Variablen newTitle zu.

Die so erzeugten Radio-Buttons müssen noch mit toForm in eine NodeSeq um-gewandelt werden.Die beiden Buttons definieren wir jeweils mit der Methode submit, die ein Labelfür den Button und eine Funktion zum Ausführen nach dem Klicken bekommt. Inbeiden Fällen nutzen wir unsere Hilfsfunktion chooseTalk. Abschließend hän-gen wir die beiden Buttons noch an die NodeSeq der Radio-Buttons an und gebendies als Ergebnis des Snippets zurück. Damit haben wir die volle Funktionalitätzur Auswahl eines Talks in den Talk Allocator implementiert.Was nun noch folgt, ist die Administrationsseite mit den Möglichkeiten, Talksanzulegen und zu löschen. Das Template dafür, das in Listing 9.17 zu sehen ist,enthält zwei Snippet-Tags. Der erste besteht aus einem Start-Tag, einem End-Tagund dazwischen ein bisschen XML. Dieses XML dazwischen wird dem SnippetTalks.add als Argument übergeben. Es enthält zwei selbst definierte Tags mitdem Präfix talk. An diesen Stellen werden wir mit der Methode add etwas ein-fügen. Der zweite Snippet-Tag (zum Löschen) hat kein Argument für das SnippetTalks.delete. Die Administrationsseite soll aussehen wie in Abbildung 9.6.

Listing 9.17: Das Template add.html für das Hinzufügen und das Löschen von Talks

<lift:surround with="default" at="content"><h2>Neuer Talk</h2><lift:Talks.add form="post"><table><tr><td>

Page 288: Scala : objektfunktionale Programmierung

9.6 Implementierung der Snippets 275

<talk:title/></td>

</tr><tr><td><talk:add/>

</td></tr>

</table></lift:Talks.add><h2>Talk löschen</h2><lift:Talks.delete form="post"/>

</lift:surround>

Abbildung 9.6: Administrationsseite des Talk Allocators

Die Methode add des Talks-Objekts ist in Listing 9.18 dargestellt. Nach einerVariable für den im Textfeld einzugebenden neuen Titel definieren wir die Hilfs-funktion addTalk. Diese erzeugt und speichert einen neuen Talk mit dem über-gebenen Titel, außer es handelt sich dabei um den leeren String oder ein Talkmit dem Titel ist bereits vorhanden. Für die Erzeugung der NodeSeq nutzen wirdie Methode bind des Objekts Helpers aus dem Package net.liftweb.util.Mit dieser Methode können wir in einer bestehenden NodeSeq Teile ändern.

Listing 9.18: Die Methode Talks.add

def add(html: NodeSeq) = {var title = ""

def addTalk(title: String) = {if (title!="" &&

Page 289: Scala : objektfunktionale Programmierung

276 9 Webprogrammierung mit Lift

Talk.find(By(Talk.title,title)).isEmpty) {Talk.create.title(title).save

}}

bind("talk",html,"title" -> text("",

t => title = t.trim),"add" -> submit("Hinzufügen",

() => addTalk(title)))

}

Das erste Argument von bind ist der Namespace. Im Template in Listing 9.17haben wir für die beiden Tags, die wir ersetzen wollen, den Namespace talkgewählt. Das zweite Argument ist die NodeSeq, in der ersetzt werden soll – inunserem Fall das Argument der Methode add mit dem Namen html. Die wei-teren Argumente sind vom Typ BindParam. Der erste davon ersetzt den Tagtitle im Namespace talk durch ein Textfeld unter Verwendung der Metho-de SHtml.text. Das erste Argument von text ist der vordefinierte Inhalt. Daszweite Argument, die Funktionalität, weist den eingegeben Text ohne führendeund schließende Leerzeichen der Variablen title zu. Der Tag talk:add wirddurch einen Button mit dem Label „Hinzufügen”, der die addTalk-Funktion aus-führt, ersetzt.

Listing 9.19: Die Methode Talks.delete

def delete = {import scala.collection.mutable.Setval toDelete = Set[Talk]()val talks = Talk.findAll

def deleteTalks(toDelete: Set[Talk]) {toDelete.foreach {talk =>if (!talk.delete_!)error("Could not delete :"+talk.toString)

}}

val checkboxes = talks.flatMap(talk =>checkbox(false,if (_) toDelete += talk

) :+Text(talk.title) :+<br />

)val delete = submit("Löschen",

Page 290: Scala : objektfunktionale Programmierung

9.6 Implementierung der Snippets 277

() => deleteTalks(toDelete))checkboxes ++ delete

}

Die Methode Talks.delete für das zweite Snippet-Tag des Templates derAdministrationsseite ist in Listing 9.19 dargestellt. Durch die Verwendung vonCheckboxen können mehrere Talks gleichzeitig gelöscht werden. Für die Talks,die gelöscht werden sollen, verwenden wir ein veränderbares Set. Zum Löschensollen ausnahmslos alle Talks zur Verfügung stehen, also auch bereits vergebene.Die Hilfsfunktion deleteTalks löscht alle übergebenen Talks. Für jeden Talk er-zeugen wir eine Checkbox, die nicht markiert ist. Falls sie markiert wurde, wirdder Talk zu toDelete hinzugefügt. Neben der Checkbox muss der Title des Talksnoch explizit angegeben werden.10 Den Button zum Löschen erzeugen wir analogzu den bisherigen Buttons.Damit sind wir mit unserer kleinen Lift-Webapplikation fertig. Wir konnten Ihnenim Rahmen dieses Kapitels natürlich nur einen kleinen Ausschnitt von Lift zeigen.Lift kann natürlich noch viel, viel mehr.

10 Es gibt noch eine überladene Version von checkbox, mit der wir eine Sequenz von Checkboxenerzeugen könnten. Diese zeigt aber den gesamten Talk und nicht nur den Titel an. Ok, wir könntennatürlich noch toString in der Klasse Talk redefinieren. Sie sehen: Es gibt immer mehrere Möglich-keiten!

Page 291: Scala : objektfunktionale Programmierung
Page 292: Scala : objektfunktionale Programmierung

Kapitel 10

LeichtgewichtigeWebprogrammierungmit Scalatra

Neben umfangreichen Webframeworks wie beispielsweise dem in Kapitel 9 vor-gestellten Lift gibt es auch leichtgewichtigere Alternativen in Scala. Eines davonist das vom Ruby-Webframework Sinatra1 inspirierte Scalatra2, das wir Ihnen indiesem Kapitel anhand eines Beispiels vorstellen wollen.Als einfaches Beispiel dient der Final-Grade-Calculator. Damit soll sich die Gesamt-note für einen der zwei Informatik-Studiengänge Bachelor of Science und Diplomaus drei Teilnoten berechnen lassen. In einer Webapplikation soll es möglich sein,den Studiengang und die drei zur Berechnung notwendigen Teilnoten zu überge-ben und die Gesamtnote mit dem erreichten Prädikat anzeigen zu lassen.In Abschnitt 10.1 werden wir zunächst unabhängig von unserem Beispiel mit ei-nem vom Scalatra-Team bereitgestellten Prototyp beginnen. Anschließend erstel-len wir den Final-Grade-Calculator und erläutern die Schritte in Abschnitt 10.2.Der dazu erstellte Code ist verfügbar unter http://github.com/obcode/finalgradecalculator_scalatra.

10.1 Quickstart mit Scalatra

Der Start mit Scalatra ist analog zu dem mit Lift (siehe 9.1). Es gibt einen Proto-typen, den wir herunterladen und anpassen können. Dazu klonen wir das Git-

1 http://www.sinatrarb.com/2 http://www.scalatra.org/

Page 293: Scala : objektfunktionale Programmierung

280 10 Leichtgewichtige Webprogrammierung mit Scalatra

Repository, wechseln in das Verzeichnis, laden per Sbt die benötigten Ressourcenherunter und starten die Webapplikation durch Start des eingebetteten Jettys:

$ git clone \git://github.com/scalatra/scalatra-sbt-prototype.git \finalgradecalculator

Cloning into finalgradecalculator......$ cd finalgradecalculator$ sbt updateGetting Scala 2.7.7 ......$ sbt jetty[info] Building project scalatra-sbt-prototype 0.1.0-

SNAPSHOT against Scala 2.8.0...

Öffnen wir anschließend die URL http://localhost:8080/ im Browser, wer-den wir mit „Hello, world!” begrüßt. Die Scalatra-Webapplikation besteht aus ei-ner einzigen Scala-Datei, deren Inhalt in Listing 10.1 angegeben ist.

Listing 10.1: Der Scalatra-Prototyp

package com.example

import org.scalatra._

class MyScalatraFilter extends ScalatraFilter {get("/") {<h1>Hello, world!</h1>

}}

Mit Scalatra können sowohl Filter als auch Servlets, basierend auf dem Java-Package javax.servlet, erstellt werden. Der Trait ScalatraFilter erwei-tert das Java-Interface Filter. Die Klasse ScalatraServlet erweitert die Java-Klasse HttpServlet aus dem Sub-Package http.Ein Scalatra-Filter oder -Servlet stellt die folgenden Methoden zur Verfügung:

before – Diese Methode wird ausgeführt, bevor ein Request zurückgegebenwird.

get(<path>) – Antwort auf einen GET-Request mit dem Pfad <path>. Be-ginnt ein Teil des Pfads mit einem Doppelpunkt, wird dieser Teil als Parameterübergeben. Wir werden dies im Abschnitt 10.2 nutzen.

post(<path>) – Antwort auf einen POST-Request mit dem Pfad <path>.

put(<path>) – Antwort auf einen PUT-Request mit dem Pfad <path>.

delete(<path>) – Antwort auf einen DELETE-Request mit dem Pfad<path>.

Page 294: Scala : objektfunktionale Programmierung

10.2 Der Final-Grade-Calculator 281

error – Wird bei einem Fehler ausgeführt.

after – Wird nach dem passenden get-, post-, put- bzw. delete-Blockausgeführt.

10.2 Der Final-Grade-Calculator

Nachdem wir den Scalatra-Prototypen zum Laufen bekommen haben, bauen wirdiesen schrittweise zum Final-Grade-Calculator (FGC) um. Dazu verschieben wirals Erstes die Klasse MyScalatraFilter an die gewünschte Stelle und passenNamen und Package an. Das Ergebnis ist Listing 10.2 zu sehen.

Listing 10.2: Die Klasse FGCFilter

package org.obraun.finalgradecalculator

import org.scalatra._

class FGCFilter extends ScalatraFilter {

get("/") {<h1>Final-Grade-Calculator</h1>

}

}

Damit der Filter auch gefunden wird, müssen wir die Datei web.xml im Verzeich-nis src/main/webapp/WEB-INF anpassen. Die gesamte Datei ist in Listing 10.3zu sehen.

Listing 10.3: Die Datei web.xml

<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-

instance"xmlns="http://java.sun.com/xml/ns/javaee"xmlns:web="http://java.sun.com/xml/ns/j2ee/web-app_2_4.

xsd"xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee

http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"version="2.4"><filter><filter-name>scalatra</filter-name><filter-class>org.obraun.finalgradecalculator.FGCFilter

</filter-class></filter>

<filter-mapping>

Page 295: Scala : objektfunktionale Programmierung

282 10 Leichtgewichtige Webprogrammierung mit Scalatra

<filter-name>scalatra</filter-name><url-pattern>/*</url-pattern>

</filter-mapping></web-app>

Starten wir nun mit Sbt den Jetty neu, so sehen wir bei der URL http://localhost:8080/ im Browser nun die Antwort des FGCFilters.Für die eigentliche Implementierung des Final-Grade-Calculators beginnen wirmit einer get-Methode, der wir die Parameter in der URL übergeben. Der Pfadsoll dazu folgendermaßen aufgebaut sein:

/calculate/grade/mark1/mark2/mark3

wobei statt des Studiengangs und der drei Noten die tatsächlichen Werte stehensollen. Das heißt, eine gültige URL wäre dann zum Beispiel:

http://localhost:8080/calculate/dipl/1,3/2,0/1,7

Dies wird in Scalatra durch den folgenden Pfad unterstützt:

/calculate/:grade/:mark1/:mark2/:mark3

In der get-Methode können wir auf die Werte dann mit der param-Methode zu-greifen. Die Reaktion auf ein GET-Request mit solch einer URL ist in Listing 10.4wiedergegeben.

Listing 10.4: Die Methode, um die Note bei einem GET-Request zu berechnen

get("/calculate/:grade/:mark1/:mark2/:mark3") {

val grade = params("grade")val mark1 = params("mark1") replace (’,’,’.’) toDoubleval mark2 = params("mark2") replace (’,’,’.’) toDoubleval mark3 = params("mark3") replace (’,’,’.’) toDouble

def isValid(mark: Double) =(1.0 <= mark) && (mark <= 5.0)

if (!isValid(mark1) ||!isValid(mark2) ||!isValid(mark3)) redirect("/error")

def rating(m: Double): String = {val adjusted = (m*10).toInt/10.0if (adjusted <= 1.2)return "mit Auszeichnung bestanden"

if (adjusted <= 1.5) return "sehr gut bestanden"if (adjusted <= 2.5) return "gut bestanden"if (adjusted <= 3.5) return "befriedigend bestanden"if (adjusted <= 4.0) return "ausreichend bestanden"return "nicht bestanden"

}

Page 296: Scala : objektfunktionale Programmierung

10.2 Der Final-Grade-Calculator 283

def calcMark(p1: Double, p2: Double, p3:Double) = {require(p1+p2+p3 == 100)val mark = ((mark1.toDouble) * p1 +(mark2.toDouble) * p2 +(mark3.toDouble) * p3

) / 100mark.toString.replace(’.’,’,’)+", "+rating(mark)

}

grade.map{_.toLower} match {case "ba" =><h1>Bachelor of Science:{calcMark(6,22,72)}

</h1>case "dipl" =><h1>Dipl.-Inform.:{calcMark(32,8,60)}

</h1>case _ => redirect("/error")

}

}

Als Erstes werden die Parameter ausgelesen und aus den Noten ein Double ge-macht. Falls es sich bei den Werten nicht um gültige Noten handelt, leiten wirum auf den Pfad /error. Die Funktion rating berechnet das Prädikat bzw. denWortlaut der Note, wobei dafür nur die ganze Note und die erste Stelle nach demKomma signifikant ist. Die Funktion calcMark berechnet die Gesamtnote ausden drei Einzelnoten, wobei die erste mit p1%, die zweite mit p2% und die drittemit p3% gewichtet wird. Für die Auswahl des Studiengangs sind die Strings baund dipl zulässig.Neben dem Pfad mit allen Parametern wollen wir auch die beiden Pfade /calcu-late/ba und /calculate/dipl zulassen. Als Reaktion soll jeweils ein Formu-lar zur Eingabe der Noten zurückgegeben werden. Der Code dafür ist in Listing10.5 dargestellt.

Listing 10.5: Reaktion auf ein GET-Request mit den Pfaden /calculate/ba oder /cal-culate/dipl

def calculateForm(gradename: String, grade: String,mark1: String, mark2: String, mark3: String

) = {<body><h1>{gradename}</h1><form action=’/post’ method=’POST’><input name=’grade’ value={grade} type=’hidden’/><table><tr>

Page 297: Scala : objektfunktionale Programmierung

284 10 Leichtgewichtige Webprogrammierung mit Scalatra

<tr><td>{mark1}:</td><td><input name=’mark1’ type=’text’/></td>

</tr><tr><td>{mark2}:</td><td><input name=’mark2’ type=’text’/></td>

</tr><tr><td>{mark3}:</td><td><input name=’mark3’ type=’text’/></td>

</tr><td></td><td><input type=’submit’/></td>

</tr></table>

</form></body>

}

get("/calculate/ba") {calculateForm("Bachelor of Science","ba","Note der Bachelorarbeit","Fachprüfungsgesamtnote","Mittlere Note aller Module"

)}

get("/calculate/dipl") {calculateForm("Diplom-Informatiker(in)","dipl","Note der Diplomarbeit","Note des Kolloquiums","Fachprüfungsgesamtnote"

)}

Die beiden Reaktionen unterscheiden sich nur an fünf Stellen. Daher haben wirdas Formular in die Funktion calculateForm herausfaktorisiert. Die beidenFormulare sind nebeneinander in Abbildung 10.1 dargestellt.Wie in Listing 10.5 zu sehen ist, wird beim Absenden des Formulars auf den Pfad/post geleitet. Wir können den bisher entwickelten Code so beibehalten undmüssen auch nichts doppelt schreiben, sondern können in dem Fall mit einemredirect reagieren. Der zusätzliche Code ist in Listing 10.6 wiedergegeben.

Page 298: Scala : objektfunktionale Programmierung

10.2 Der Final-Grade-Calculator 285

Abbildung 10.1: Eingabeformulare für Bachelor und Diplom

Listing 10.6: Reaktion auf ein POST-Request mit dem Pfad /post

post("/post") {redirect("/calculate/"+params("grade")+"/"+params("mark1")+"/"+params("mark2")+"/"+params("mark3")

)}

Wir ermitteln die Parameter und erzeugen daraus einen Pfad, auf den der Blockaus Listing 10.4 reagieren muss.Um bereits an der Wurzel des Pfades etwas eingeben zu können, ändern wir dieReaktion auf den Pfad / ab, wie in Listing 10.7 dargestellt ist.

Listing 10.7: Reaktion auf ein GET-Request mit dem Pfad /

get("/") {<body><h1>Final-Grade-Calculator</h1><form action=’/postgrade’ method=’POST’><input type=’radio’ name=’grade’ value=’ba’ />

Bachelor of Science<input type=’radio’ name=’grade’ value=’dipl’ />

Diplom-Informatiker<input type=’submit’/>

</form></body>

}

So starten wir bei Eingabe von http://localhost:8080/ mit dem Formularaus Abbildung 10.2.Zu guter Letzt benötigen wir noch die Reaktion auf den POST-Request mit demPfad /postgrade, den das Absenden des Formulars aus Listing 10.7 auslöst. Die-

Page 299: Scala : objektfunktionale Programmierung

286 10 Leichtgewichtige Webprogrammierung mit Scalatra

Abbildung 10.2: Auswahlmöglichkeit des Studiengangs

se Reaktion und eine Reaktion auf alle sonstigen Requests mithilfe der notFound-Methode sind in Listing 10.8 zu sehen.

Listing 10.8: Reaktion auf ein POST-Request mit dem Pfad /postgrade und Reaktion aufalle bisher nicht definierten Requests

post("/postgrade") {redirect("/calculate/"+params("grade"))

}

notFound {"Falsche Eingabe"

}

Auch wenn Scalatra bei Weitem nicht so umfangreich ist wie Lift, konnten wir hiernur einen ersten Einstieg bieten und nicht alles behandeln. Über das Besprochenehinaus bietet die aktuelle Scalatra-Version Unterstützung für das Hochladen vonDateien, die Verwaltung von Sessions und das Einbinden der Template EngineScalate3.

3 http://scalate.fusesource.org/

Page 300: Scala : objektfunktionale Programmierung

Kapitel 11

Akka – Actors und SoftwareTransactional Memory

Nachdem wir Scalas eingebaute Actors in Kapitel 7 kennengelernt haben, werfenwir im vorliegenden Kapitel einen Blick auf das Framework Akka1, das auf demActors-Model basiert und den Standardansatz von Scala noch um einiges weiterführt. Selbsterklärtes Ziel von Akka ist einfachere Skalierbarkeit, Fehlertoleranz,Nebenläufigkeit und Zugriff über das Netzwerk durch die Verwendung von Ac-tors.Durch die Verbindung von Actors und Software Transactional Memory (STM)2

wird der Abstraktionslevel erhöht und damit eine Basis geschaffen, auf der es ein-facher ist, korrekte nebenläufige und skalierbare Anwendungen zu schreiben. Fürdie Fehlertoleranz setzt Akka auf das im Bereich der Telekommunikation bewähr-te Konzept Let it crash3. Und letztendlich können Akka-Actors völlig transparentverteilt werden.Auf den folgenden Seiten wollen wir Ihnen einen Einstieg in Akka geben. Analogzu den vorangegangenen Kapiteln werden wir schrittweise eine Beispielapplika-tion entwickeln und dabei auf die wesentlichen Konzepte eingehen. Dazu bauenwir auf der in Kapitel 8 (siehe Listing 8.7 auf Seite 229) verwendeten einfachenVideotheksverwaltung (MovieStore) auf und erweitern diese.In Abschnitt 11.1 werden wir ein Akka-Projekt mit Sbt aufsetzen, um dann denMovieStore in Abschnitt 11.2 in einen Akka-Actor zu verwandeln. Mit demBenutzer- und Sitzungsmanagement beschäftigen wir uns in Abschnitt 11.3. In

1 http://akkasource.org/2 STM ist ein Ansatz, bei dem der Zugriff auf gemeinsam genutzten Speicher in Transaktionen analogzu Datenbanktransaktionen erfolgt. Siehe auch [ST95].3 Die Idee von Let it crash oder Embrace failure kommt von der Programmiersprache Erlang (siehe auch[Arm07]). Dort werden Supervisoren genutzt, die beispielsweise beim Abbruch eines Prozesses auchandere Prozesse terminieren können, um dann alle wieder geordnet zu starten.

Page 301: Scala : objektfunktionale Programmierung

288 11 Akka – Actors und Software Transactional Memory

Abschnitt 11.4 verwenden wir Akkas Software Transactional Memory, um die Da-tenstrukturen für verfügbare und entliehene Filme aus mehreren MovieStoresheraus gemeinsam nutzen zu können, ohne Gefahr zu laufen, einen inkonsisten-ten Zustand zu erzeugen. Schließlich zeigen wir Ihnen in Abschnitt 11.5 die Im-plementierung des Services und des Clients und wie sich die gesamte Applicationdann in der Scala-Shell ausprobieren lässt. Der in diesem Kapitel erstellte Code istverfügbar unter http://github.com/obcode/moviestore_akka.

11.1 Quickstart mit Akka

Für die Entwicklung unserer Akka-Applikation nutzen wir wieder Sbt. Wir erzeu-gen zunächst ein neues Projekt in einem leeren Verzeichnis:

$ mkdir moviestore$ cd moviestore$ sbtProject does not exist, create new project? (y/N/s) yName: moviestoreOrganization: org.obraunVersion [1.0]: 0.9Scala version [2.7.7]: 2.8.0sbt version [0.7.4]:Getting Scala 2.7.7 ...

Als Nächstes konfigurieren wir unser Projekt so, dass das Akka-Plugin genutztwerden kann. Die dazu notwendige Datei Plugins.scala ist in Listing 11.1 zusehen.

Listing 11.1: Die Datei project/plugins/Plugins.scala

import sbt._

class Plugins(info: ProjectInfo)extends PluginDefinition(info) {

val akkaPlugin = "se.scalablesolutions.akka" %"akka-sbt-plugin" % "0.10"

}

Mit dem Akka-Plugin, das zur verwendeten Akka-Version passen muss, kann dasSbt-Projekt durch Hineinmixen des AkkaProject-Traits zu einem Akka-Projektgemacht werden. Die dazu notwendige Datei MyProject.scala ist in Listing11.2 dargestellt.

Page 302: Scala : objektfunktionale Programmierung

11.2 Der MovieStore 289

Listing 11.2: Die Datei project/build/MyProject.scala

import sbt._

class MyProject(info: ProjectInfo)extends DefaultProject(info) with AkkaProject

Mit dieser Definition haben wir eine Abhängigkeit zum Core-Modul von Ak-ka definiert. Abhängigkeiten zu weiteren Modulen können mit der vom TraitAkkaProject zur Verfügung gestellten Methode akkaModule hinzugefügt wer-den, z.B.

val akkaKernel = akkaModule("kernel")

um den Akka-Mikrokernel zu verwenden, der es ermöglicht, Akka als Stand-alone-Service zu nutzen. Für unsere Beispielapplikation benötigen wir keine wei-teren Akka-Module. Nach einem sbt update steht alles lokal zur Verfügung,und wir können mit der Entwicklung des MovieStores beginnen.

11.2 Der MovieStore

Wir starten mit dem MovieStore, wie wir ihn in Kapitel 8 entwickelt hatten. UmIhnen das Zurückblättern zu ersparen, haben wir den Inhalt von Listing 8.7 aufSeite 229 in Listing 11.3 noch einmal wiedergegeben.

Listing 11.3: Die Klassen Movie und MovieStore aus Kapitel 8

case class Movie(title: String, filmrating: Int)

class MovieStore {

private[this] var available = Map[Int,Movie]()private[this] var rent = Map[Int,Movie]()

def addToStore(movie: Movie) {MovieStore.serial += 1available += (MovieStore.serial -> movie)

}

def addToStore(movies: Traversable[Movie]) {movies foreach (addToStore(_))

}

def rentMovie(serial: Int): Option[Movie] = {val movieOption = available get serialmovieOption match {case None => Nonecase Some(movie) =>available -= serial

Page 303: Scala : objektfunktionale Programmierung

290 11 Akka – Actors und Software Transactional Memory

rent += (serial -> movie)movieOption

}}def returnMovie(serial: Int) = {val movie = rent(serial)rent -= serialavailable += (serial -> movie)

}

def availableMoviesForAge(age: Int) =available.filter{case (_,Movie(_,r)) => r <= age

}

def availableMovies = available

def rentMovies = rent

}object MovieStore {private var serial = 0

}

Wir bauen den MovieStore nun für unsere Akka-Anwendung um. Der Movie-Store-Dienst soll später neben der Verwaltung der Filme auch eine Benutzerver-waltung haben. Für jeden Benutzer, der den Dienst nutzt, soll eine eigene Sessionerzeugt werden. All dies wollen wir nicht in einer Klasse zusammen implemen-tieren. Daher machen wir als Erstes aus der Klasse MovieStore einen Trait. DasMovieStore-Objekt und die Klasse Movie übernehmen wir unverändert.Im Trait MovieStore wollen wir die Reaktion auf Nachrichten, die die Verwal-tung der Filme betrifft, bereits implementieren. Daher erweitern wir ihn mit demTrait Actor aus dem Akka-Core-Package. Der Trait MovieStore sieht in seinerersten Mutation so aus, wie in Listing 11.4 dargestellt ist.

Listing 11.4: Erste Änderungen am MovieStore

package org.obraun.moviestore

import se.scalablesolutions.akka.actor.Actor

trait MovieStore extends Actor {

protected[this] var available = Map[Int,Movie]()protected[this] var rent = Map[Int,Movie]()

def rentMovieAge(age: Int, serial: Int) = {val movieOption = available get serialmovieOption match {case None => None

Page 304: Scala : objektfunktionale Programmierung

11.2 Der MovieStore 291

case Some(movie @ Movie(_,r)) =>if (r <= age) {available -= serialrent += (serial -> movie)movieOption

} else None}

}...

}

Nach der Definition des Packages importieren wir den Akka-Actor-Trait. Diebeiden Maps sollen nun nicht mehr private sein, sondern vererbt werdenkönnen. Die Methode rentMovieAge ist neu und löst die Methode rent-Movie ab, die das Alter beim Ausleihen nicht beachtete. Die Methoden add-ToStore, returnMovie und availableMoviesForAge übernehmen wir un-verändert. Daher haben wir sie in Listing 11.4 ausgespart. Die beiden MethodenavailableMovies und rentMovies übernehmen wir nicht.Was zur Definition des Actors noch fehlt, ist die Implementierung der Methode

def receive: PartialFunction[Any, Unit]

die als einzige abstrakte Methode im Akka-Actor-Trait enthalten ist. Die Im-plementierung dieser Methode entspricht dem, was in Scala-Actors mit einemreceive- bzw. react-Block gemacht wird. Bevor wir uns mit der Implementie-rung beschäftigen, definieren wir zunächst einmal die verschiedenen Messages,auf die der MovieStore-Trait reagieren bzw. die er versenden können soll. DieMessages sind in Listing 11.5 dargestellt. Wir werden später noch weitere Messa-ges ergänzen.

Listing 11.5: Die Messages für den MovieStore-Trait

package org.obraun.moviestore

sealed trait Message

case class AvailableList(age: Int) extends Messagecase class RentMovie(age: Int, serial: Int)extends Message

case class Return(serial: Int) extends Message

case class ResultList(movies: List[(Int,String,Int)])extends Message

case class SuccessfullyRent(serial: Int) extends Messagecase class Error(msg: String) extends Message

Die drei Messages AvailableList, RentMovie und Return sollen vom Mo-vieStore empfangen werden. Die Messages ResultList, Successfully-Rent und Error werden für Antworten genutzt. Die Reaktion auf die Messages

Page 305: Scala : objektfunktionale Programmierung

292 11 Akka – Actors und Software Transactional Memory

definieren wir in der Methode rentalManagement, die im Trait MovieStoredann auch als Implementierung von receive dient. Die beiden Methoden sindin Listing 11.6 dargestellt.

Listing 11.6: Die Methoden receive und rentalManagement des MovieStore-Traits

def receive = rentalManagement

protected def rentalManagement: Receive = {case AvailableList(age) =>val result = availableMoviesForAge(age)println("Calculated "+result)self.reply(ResultList(result.toList.map {case (s,movie) =>(s,movie.title,movie.filmrating)

})

)case RentMovie(age, serial) =>val maybeMovie = rentMovieAge(age,serial)maybeMovie match {case None =>self.reply(Error("Movie not available"))

case Some(movie) =>self.reply(SuccessfullyRent(serial))

}case Return(serial) => returnMovie(serial)

}

Der Typ Receive ist ein Typsynonym für PartialFunction[Any, Unit].Wie auch schon bei den Scala-Actors reagieren wir mithilfe von Pattern Mat-ching auf die verschiedenen Messages. Etwas Neues ist der Ausdruck self.-reply(...). In Akka werden Actors über ActorRefs gesteuert. Das sind Re-ferenzen auf Actors. Das self-Feld eines Actors enthält die Referenz auf sichselbst. Im ActorRef-Trait gibt es dann die Methoden wie ! zum Senden von Mes-sages oder, wie in Listing 11.6 verwendet, reply zum Senden einer Message anden Sender der empfangenen Message.Auf die Message AvailableList wird die Map der verfügbaren, für das Altererlaubten Filme ermittelt. Diese wird in eine Liste von Tripeln umgewandelt, dieaus Seriennummer, Titel und der Altersbeschränkung besteht. Dies machen wir,um die interne Repräsentation von Filmen später noch verändern zu können, oh-ne den Client anpassen zu müssen. Die Liste wird dann in eine ResultList-Message verpackt und mit self.reply zurückgesendet.Bei einer RentMovie-Message wird – je nachdem, ob der Film ausgeliehen wer-den kann bzw. darf – mit einer SuccessfullyRent- oder Error-Message geant-

Page 306: Scala : objektfunktionale Programmierung

11.3 User- und Session-Management 293

wortet. Beim Zurückgeben eines Films über die Message Return ist keine Ant-wort vorgesehen.

11.3 User- und Session-Management

Als Nächstes wollen wir ein Session-Management implementieren. Dazu benöti-gen wir eine Klasse User, deren Implementierung in Listing 11.7 dargestellt ist.

Listing 11.7: Die Klasse und das Objekt User

package org.obraun.moviestore

import java.util.Calendar

class User private (val name: String,val dateOfBirth: Calendar,val id: Int

) {override def toString = id+": "+name+" ("+age+")"def age = {val today = Calendar.getInstanceval age = today.get(Calendar.YEAR) -

dateOfBirth.get(Calendar.YEAR)today.set(Calendar.YEAR,

dateOfBirth.get(Calendar.YEAR))if (today before dateOfBirth)age-1

elseage

}}object User {private[this] var id = 0def apply(name: String, dateOfBirth: Calendar) = {id += 1new User(name, dateOfBirth, id)

}def unapply(x: Any) = {x match {case c: User => Some(c.name,c.dateOfBirth,c.id)case _ => None

}}

}

Ein User hat eine ID, einen Namen und einen Geburtstag. Mit der Methode agelässt sich das Alter des Users berechnen. Nachdem die ID eindeutig sein soll,verhindern wir mit dem private-Modifier vor den Klassen-Parametern, dass ei-

Page 307: Scala : objektfunktionale Programmierung

294 11 Akka – Actors und Software Transactional Memory

ne Instanz der Klasse mit new erzeugt werden kann. Stattdessen definieren wirim Companion-Objekt eine apply-Methode, die ein Objekt mit einer eindeutigenID erzeugt und zurückgibt. Die unapply-Methode definieren wir, damit wir mitUser-Objekten Pattern Matching machen können.Für das Session-Management benötigen wir noch einige Messages. Diese sind inListing 11.8 dargestellt. Mit den Messages Login und Logout kann sich ein Be-nutzer an- bzw. abmelden. Mit ShowAvailable fragt der Benutzer die Liste derfür ihn zulässigen Filme ab. Mit Rent wird ein Film ausgeliehen.

Listing 11.8: Zusätzliche Messages für den Trait SessionManagement

case class Login(userID: Int) extends Messagecase class Logout(userID: Int) extends Message

case class ShowAvailable(userID: Int) extends Messagecase class Rent(userID: Int, serial: Int) extends Message

Die Implementierung des Traits SessionManagement ist in Listing 11.9 angege-ben.

Listing 11.9: Der Trait SessionManagement

package org.obraun.moviestore

import se.scalablesolutions.akka.actor.{Actor,ActorRef}import se.scalablesolutions.akka.actor.Actor.actorOf

trait SessionManagement extends Actor {

protected var users: Map[Int,User]protected var sessions = Map[Int,ActorRef]()

abstract override def receive =sessionManagement orElse super.receive

def sessionManagement: Receive = {case Login(id) =>val user = users.get(id)user match {case None =>self.reply(Error("User id "+id+" not known!"))

case Some(user) =>sessions.get(id) match {case None =>log.info("User [%s] has logged in",

user.toString)val session = actorOf(new Session(user,self)

)session.start

Page 308: Scala : objektfunktionale Programmierung

11.3 User- und Session-Management 295

sessions += (id -> session)case _ =>

}}

case Logout(id) =>log.info("User [%d] has logged out",id)val session = sessions(id)session.stopsessions -= id

case msg @ ShowAvailable(userID) =>sessions(userID) forward msg

case msg @ Rent(userID, _) =>sessions(userID) forward msg

}

override def shutdown = {log.info("Sessionmanagement is shutting down...")sessions foreach { case (_,session) => session.stop }

}}

Der Trait SessionManagement erweitert den Trait Actor. Das abstrakte Feldusers entspricht einer Mapmit UserIDs als Schlüssel und User-Objekten als Wer-ten. Das Session-Management empfängt im Wesentlichen Login- und Logout-Messages und erzeugt und beendet die dazugehörigen Sessions. Die Implemen-tierung der Sessions werden wir im Anschluss an das Session-Management be-sprechen. Die gerade existierenden Sessions werden im sessions-Feld in einerMap gespeichert, die den UserIDs die dazugehörige Session als ActorRef zuord-net.Um auf zusätzliche Messages reagieren zu können, wird die Methode receiveabstrakt redefiniert. Damit ist der Trait SessionManagement stapelbar (sieheauch Abschnitt 4.3.2 auf Seite 88). Nachdem receive eine partielle Funktionist, können wir die Methode orElse nutzen. Damit wird auf eine Message dieMethode sessionManagement angewendet, wenn sie an der Stelle, d.h. für die-se Message, definiert ist. Sonst wird die in der jeweiligen Klasse, die den TraitSessionManagement hinzufügt, definierte Methode receive durch den Aus-druck super.receive genutzt.Das eigentliche Management der Sessions erfolgt in der Methode session-Management. Auf die Message Login hin wird versucht, den zur übergebenenid gehörenden User zu ermitteln. Ist dies nicht möglich, wird eine Fehlermel-dung zurückgesendet. Ist dies der Fall, wird überprüft, ob für die id bereits eineSession besteht. Nur wenn keine Session gefunden wird, wird als Erstes mit derMethode log aus dem Trait Actor eine Meldung ausgegeben. Wo diese Logmel-dung dann einmal landet, kann auf der Akka-Plattform konfiguriert werden. An-schließend wird eine neue Session erzeugt, gestartet und in der sessions-Mapgespeichert.

Page 309: Scala : objektfunktionale Programmierung

296 11 Akka – Actors und Software Transactional Memory

Eine Session ist auch ein Actor. Um in Akka eine Instanz eines Actors zuerzeugen, wird eine Funktion, die den Actor erzeugen kann, in unserem Fallnew Session, an die Methode actorOf des Actor-Companion-Objekts über-geben. Diese erzeugt den Actor und gibt die dazugehörige ActorRef zurück.Über die ActorRef kann der Actor anschließend gestartet werden. Die Klassen-Parameter von Session sind, wie wir weiter unten sehen werden, der User unddie Referenz auf den MovieStore.Als Reaktion auf die Logout-Message wird die Session gestoppt und entfernt.Die beiden Messages ShowAvailable und Rentwerden an die jeweilige Sessionweitergeleitet. Durch die Verwendung der ActorRef-Methode forward bleibtder Absender der Message unverändert. Aufgrund dessen kommt ein reply alsReaktion in der Session nicht beim SessionManagement, sondern beim ur-sprünglichen Absender an.Wenn der SessionManagement-Actor mit stop beendet wird, müssen die an-ders nicht erreichbaren Sessions auch beendet werden. Dies implementieren wirdurch Redefinition der Methode shutdown aus dem Actor-Trait.

Listing 11.10: Die Klasse Session

package org.obraun.moviestore

import se.scalablesolutions.akka.actor.{Actor,ActorRef}

class Session(user: User, moviestore: ActorRef)extends Actor {

private[this] val age = user.age

def receive = {case msg @ ShowAvailable(_) =>self.reply(moviestore !! AvailableList(age) getOrElseError("Cannot show movies!")

)case Rent(_, serial) =>self.reply(moviestore !! RentMovie(age, serial) getOrElseError("Cannot rent movie #"+serial)

)}

}

Die Klasse Session ist in Listing 11.10 zu sehen. Das Alter des Users wird nureinmal berechnet, und zwar beim Erzeugen der Session. Die receive-Methodereagiert auf die beiden Messages, die vom SessionManagement an die Sessi-on geschickt werden können. Sinn und Zweck in beiden Fällen ist die Anreiche-rung der Anfrage um das Alter des Benutzers. Damit kann der Benutzer bei einer

Page 310: Scala : objektfunktionale Programmierung

11.4 Software Transactional Memory 297

Anfrage nicht „schummeln” und bekommt wirklich nur die Filme, die für ihnzugelassen sind. Die Session sendet die Anfragen an die ActorRef, die denMovieStore repräsentiert. Die dazu verwendete Methode !! wartet auf eineAntwort. Diese wird mit reply zurückgesendet. Nachdem es sich bei der Ant-wort um eine Option handelt, wird diese zum Weiterschicken entweder ausge-packt oder durch eine Error-Message ersetzt.

11.4 Software Transactional Memory

Mit Software Transactional Memory (STM) wird die Idee der Datenbank-Trans-aktion in den Hauptspeicher übertragen, um mit mehreren Threads auf dieselbenDaten zugreifen zu können. Die schwierige Aufgabe, Locks zu setzen und dabeiDeadlocks zu vermeiden, wird damit unnötig. Von den ACID4-Eigenschaften ei-ner Datenbanktransaktion bieten STM-Transaktionen die drei ersten (ACI). DieDauerhaftigkeit macht im Hauptspeicher natürlich wenig Sinn. Wichtig ist aber,dass die Änderungen für alle Threads sichtbar sind.Mit Unterstützung von STM können wir unsere Videotheksverwaltung sehr ein-fach so umbauen, dass für jeden Client über die Session hinaus auch ein eigenerMovieStore-Actor gestartet wird, der die Requests des Clients unabhängig vonallen anderen behandelt. Die Maps mit den verfügbaren und verliehenen Filmensollen aber von allen gemeinsam genutzt werden.Die Herausforderung ist also, die beiden Datenstrukturen so zu verwalten, dassalle darauf zugreifen können, aber der Zustand immer konsistent ist. Das heißtzum Beispiel, beim Ausleihen muss ein Film aus available heraus- und inrent hineingenommen werden. Dies kann mit STM in einer Transaktion erfolgen,d.h. entweder war beides erfolgreich oder der Zustand vorher ist für die anderenActors sichtbar.Was wir dazu benötigen, sind transaktionale Referenzen (Instanzen der KlasseRef), in denen die Daten gespeichert werden. Zugriff auf solche Refs ist nurinnerhalb von Transaktionen möglich. Der Wert, den eine Ref aufnimmt, sollteselbst unveränderbar sein. Die ersten Änderungen, die wir am Trait MovieStorevornehmen, ist das Umwandeln der beiden Felder available und rent intransaktionale Referenzen (siehe Listing 11.11).

Listing 11.11: Die Felder available und rent des Traits MovieStore als transaktionaleReferenzen

import se.scalablesolutions.akka.stm.local.Refprotected val available: Ref[Map[Int,Movie]]protected val rent: Ref[Map[Int,Movie]]

Gegenüber der ursprünglichen Version haben wir drei Dinge verändert:

4 Atomic – Consistent – Isolated – Durable

Page 311: Scala : objektfunktionale Programmierung

298 11 Akka – Actors und Software Transactional Memory

1. Wir haben aus der var einen val gemacht. Die Ref muss immer dieselbebleiben. Der Inhalt ändert sich.

2. Wir haben den Typ von einer Map auf eine Ref verändert.

3. Wir haben die beiden Felder zu abstrakten Feldern gemacht. Dies ist natürlichnicht notwendig, um STM zu nutzen. Wir könnten auch hier gleich die Referzeugen, aber wir wollen dieselbe Ref ja auch in den anderen MovieStoresverwenden.

Um über die Referenzen auf den Inhalt zu kommen, müssen wir die Methode getverwenden. Den Inhalt einer Referenz verändern wir mit der Methode alter.Schließlich verwenden wir noch atomic, um einen Block zu einer Transaktionzu machen. Die im Vergleich zum bisher entwickelten MovieStore verändertenMethoden sind in Listing 11.12 dargestellt.

Listing 11.12: Die Methoden des Traits MovieStore, die für die Einbindung von STMverändert werden mussten

import se.scalablesolutions.akka.stm.local.atomic

def addToStore(movie: Movie) {MovieStore.serial += 1atomic {available alter (_ + (MovieStore.serial -> movie))

}}

def rentMovieAge(age: Int, serial: Int): Option[Movie] =atomic {val movieOption = available.get.get(serial)movieOption match {case None => Nonecase Some(movie @ Movie(_,r)) =>if (r <= age) {available alter (_ - serial)rent alter (_ + (serial -> movie))movieOption

} else None}

}

def returnMovie(serial: Int) = {atomic {val movie = rent.get.apply(serial)rent alter (_ - serial)available alter (_ + (serial -> movie))

}}

Page 312: Scala : objektfunktionale Programmierung

11.4 Software Transactional Memory 299

def availableMoviesForAge(age: Int) =atomic {available.get.filter{case (_,Movie(_,r)) => r <= age

}}

Alle Code-Blöcke, in denen wir auf available und rent zugreifen, umschlie-ßen wir mit atomic und machen so eine Transaktion daraus. Die Seriennummerim MovieStore-Companion-Objekt haben wir nicht zu einer transaktionalen Re-ferenz gemacht. Stattdessen stellen wir in unserem Service (siehe Abschnitt 11.5)sicher, dass addToStore nur im „Haupt-MovieStore” genutzt werden kann,um eine Seriennummer zu generieren und einen neuen Film einzufügen.Um einen Film zu den verfügbaren Filmen hinzuzufügen, nutzen wir alter miteiner Funktion als Argument. Diese Funktion bekommt als Parameter den altenInhalt und berechnet als Ergebnis den neuen.Zum Ausleihen eines Films machen wir aus dem gesamten Rumpf der Metho-de rentMovieAge eine Transaktion, da wir erst einmal überprüfen, ob der Filmverfügbar ist, und ihn dann ausleihen. Beim Ermitteln des Films ist das erste getnotwendig, um die Map aus der Ref zu holen. Das zweite get greift dann mitdem Schlüssel auf die Map zu und gibt eine Option[Movie] zurück. Außerdemmüssen wir die Änderungen an available und rent wieder mit alter durch-führen.Die Änderungen an returnMovie und availableMoviesForAge sind analogzu den Änderungen an den anderen beiden Methoden: get für den Zugriff undalter für die Veränderung. Mehr müssen wir in unserer Videotheksverwaltungnicht anpassen, um STM für die Maps zu nutzen.Um im Session-Management statt des bestehenden MovieStores einen neuenfür jede Session zu erzeugen, ändern wir im Trait SessionManagement genauzwei Stellen:

1. Für die Erzeugung der neuen Session nutzen wir nicht self als ActorRef,sondern übergeben einen neu erzeugten MovieStore:

val session = actorOf(new Session(user,newMovieStore)

)

2. Zur Erzeugung des neuen MovieStores fügen wir die unter Punkt 1 schongenutzte abstrakte Methode

def newMovieStore: ActorRef

ein.

Page 313: Scala : objektfunktionale Programmierung

300 11 Akka – Actors und Software Transactional Memory

An der Klasse Session nehmen wir noch eine Änderung vor: Wenn ein Ses-sion-Actor gestoppt wird, muss dieser seinen MovieStore stoppen. Dazu re-definieren wir die Methode shutdown wie folgt:

override def shutdown = moviestore.stop

Alles, was darüber hinaus noch notwendig ist, werden wir im Service und imClient implementieren, die wir im folgenden Abschnitt besprechen.

11.5 Client und Service

Nachdem wir die gesamte Funktionalität bereits implementiert haben, müssenwir diese für den MovieStoreService nur noch zusammensetzen:

package org.obraun.moviestore

class MovieStoreServiceextends MovieStorewith SessionManagement

In der Klasse MovieStoreService müssen wir für die Felder available undrent aus dem Trait MovieStore je einen Wert angeben:

import se.scalablesolutions.akka.stm.local.Refprotected val available = Ref(Map[Int,Movie]())protected val rent = Ref(Map[Int,Movie]())

Um das abstrakte Feld users aus SessionManagement mit einem konkretenWert zu versehen und einige Filme zum Ausprobieren hinzufügen zu können,definieren wir den Trait ExampleData, der in Listing 11.13 wieder gegeben ist.

Listing 11.13: Der Trait ExampleData mit einigen Beispielbenutzern und -filmen

package org.obraun.moviestore

trait ExampleData {

import java.util.{Calendar,GregorianCalendar}

var users = List(User("Jon Spencer",new GregorianCalendar(1970,Calendar.APRIL,12)

),User("Ilse Kling",new GregorianCalendar(1997,Calendar.MAY,17)

)) map {case user @ User(_,_,id) => (id -> user)} toMap

Page 314: Scala : objektfunktionale Programmierung

11.5 Client und Service 301

val movies = Set(Movie("Step Across the Border",0),Movie("Am Limit",6),Movie("The Matrix",16),Movie("Bad Taste",18),Movie("Bad Lieutenant",16)

)}

Den Trait ExampleData fügen wir zur Klasse MovieStoreService hinzu.Der Ausdruck addToStore(movies) im Rumpf der Klasse stellt die Beispiel-filme dann im MovieStore zur Verfügung. Schließlich müssen wir noch eineImplementierung für die abstrakte Methode newMovieStore aus Session-Management angeben (siehe Listing 11.14).

Listing 11.14: Die Methode newMovieStore zum Erzeugen eines neuen MovieStore-Actors mit den transaktionalen Referenzen

import se.scalablesolutions.akka.actor.Actor.actorOf

def newMovieStore =actorOf(new MovieStore {protected val available: Ref[Map[Int,Movie]] =service.available

protected val rent: Ref[Map[Int,Movie]] =service.rent

override def addToStore(movie: Movie) = ()override def addToStore(movies: Traversable[Movie]

) = ()}

).start

Die Methode newMovieStore erzeugt einen Actor aus einer anonymen Klasse,die den Trait MovieStore hinzufügt. Die beiden Maps available und rentwerden auf die Referenz des MovieStoreServices gesetzt. Dies können wir sonatürlich nicht mit this.available bzw. this.rent machen. Daher müssenwir uns noch eine Self-Type-Annotation (siehe Abschnitt 5.7.8) mit einem anderenBezeichner definieren. Der üblicherweise verwendete Bezeichner self kann ineinem Akka-Actor nicht verwendet werden, da es sich dabei um die ActorRefhandelt. Wir haben daher den Bezeichner service gewählt, d.h. die Definitionder Klasse beginnt mit

class MovieStore ... { service =>

Damit in den so erzeugten MovieStores keine Filme hinzugefügt werden kön-nen, redefinieren wir addToStore mit dem konstanten Wert () vom Typ Unit.Der erzeugte Actor wird schließlich noch mit start gestartet.

Page 315: Scala : objektfunktionale Programmierung

302 11 Akka – Actors und Software Transactional Memory

Um den MovieStoreService als Service über das Netzwerk zur Verfügung zustellen, müssen wir nur noch die init-Methode, wie in Listing 11.15 dargestellt,redefinieren. Wir starten ein RemoteNode auf dem lokalen Rechner am Port 9999und registrieren die self-ActorRef unter dem Namen moviestore:service.

Listing 11.15: Redefinition der Methode init im MovieStoreService

override def init = {import se.scalablesolutions.akka.remote.RemoteNodeRemoteNode.start("localhost",9999)RemoteNode.register("moviestore:service",self)

}

Da wir mit Sbt arbeiten, können wir per sbt console eine Scala-Shell starten,in der alle Bibliotheken und Klassen unseres Projekts verfügbar sind, um unserenMovieStoreService zu testen. Die folgende Sitzung zeigt den Start des Ser-vices im Akka-Framework:

scala> import org.obraun.moviestore._import org.obraun.moviestore._

scala> import se.scalablesolutions.akka.actor.Actor._import se.scalablesolutions.akka.actor.Actor._

scala> val movieStoreService =| actorOf[MovieStoreService].start

...00:22:31.065 [run-main] INFO s.s.akka.remote.RemoteNode$

- Registering server side remote actor [org.obraun.moviestore.MovieStoreService] with id [moviestore:service]

00:22:31.066 [run-main] DEBUG s.s.akka.actor.Actor$ - [Actor[org.obraun.moviestore.MovieStoreService:1283512328095]] has started

...

Um den Service testen zu können, implementieren wir einen einfachen Client, denwir in einer weiteren Scala-Shell nutzen können. Der Client-Code ist in Listing11.16 dargestellt.

Listing 11.16: Der MovieStoreClient

package org.obraun.moviestore

import se.scalablesolutions.akka.remote.RemoteClient

class MovieStoreClient(userID: Int) {

val movieStore =RemoteClient.actorFor(

Page 316: Scala : objektfunktionale Programmierung

11.5 Client und Service 303

"moviestore:service","localhost",9999

)

def login = movieStore ! Login(userID)

def logout = movieStore ! Logout(userID)

def rent(serial: Int) = {val result = movieStore !! Rent(userID, serial)result match {case Some(SuccessfullyRent(_)) =>println("Successfully rent movie #"+serial)

case Some(Error(msg)) =>println("error: "+msg)

case msg =>println("error: something unexpected happened "

+msg)}

}

def show = {val result = movieStore !! ShowAvailable(userID)result match {case Some(ResultList(movies)) =>for((serial,title,filmrating) <- movies)println(serial+" "+title+" ("+filmrating+")")

case msg =>println("error while receiving movielist: "+msg)

}}

def returnM(serial: Int) = movieStore ! Return(serial)

}

In der Klasse MovieStoreClient wird die Verbindung zum MovieStore-Service über das RemoteClient-Objekt hergestellt. Auch wenn der Client indiesem Beispiel auf dem gleichen Host läuft, könnte er so auch auf einem anderenRechner laufen. Es müsste dann nur localhost durch den Namen des Rechnersersetzt werden, auf dem der MovieStoreService läuft.Die drei Methoden login, logout und returnM senden die jeweils notwen-dige Message mit ! an den MovieStoreService und warten somit nicht aufeine Antwort. Die beiden Methoden rent und show senden ihre Message mit!! und weisen das Ergebnis, die Antwort-Message, einem val zu. Mithilfe vonPattern Matching wird die Antwort ausgewertet und entsprechend mit printlnausgegeben. Zum Ausprobieren starten wir in einem zweiten Terminal mit sbtconsole eine weitere Scala-Shell und können, wie in der folgenden Sitzung dar-gestellt ist, mit dem Service arbeiten:

Page 317: Scala : objektfunktionale Programmierung

304 11 Akka – Actors und Software Transactional Memory

scala> import org.obraun.moviestore._import org.obraun.moviestore._

scala> val client = new MovieStoreClient(1)...INFO: Successfully initialized GlobalStmInstance using

factoryMethod ’org.multiverse.stms.alpha.AlphaStm.createFast’.

client: org.obraun.moviestore.MovieStoreClient = org.obraun.moviestore.MovieStoreClient@51493995

scala> import client._import client._

scala> login11:33:58.819 [run-main] DEBUG s.s.akka.remote.

RemoteClientHandler - [id: 0x273d6d53] OPEN11:33:58.860 [run-main] INFO s.s.akka.remote.

RemoteClient - Starting remote client connection to [localhost:9999]

...

scala> show...1 Am Limit (6)2 Bad Taste (18)3 The Matrix (16)4 Bad Lieutenant (16)5 Step Across the Border (0)

scala> rent(1)...Successfully rent movie #1

scala> show...2 Bad Taste (18)3 The Matrix (16)4 Bad Lieutenant (16)5 Step Across the Border (0)

Sie können auch noch weitere Scala-Shells öffnen und sich von dort mit einer an-deren UserID oder auch mit derselben anmelden.An dieser Stelle wollen wir unseren kurzen Blick auf das sehr mächtige Akka-Framework beenden, auch wenn wir viele weitere interessante Features wiebeispielsweise den Akka-Microkernel, Persistenz, Lift-Integration und Fehlerto-leranz im Rahmen dieses Buches nicht besprechen konnten. Die unter http://doc.akkasource.org/ bereitgestellte Dokumentation ist sehr gut und um-fangreich, sodass damit einer Vertiefung Ihrer Kenntnisse im Bereich Akka nichtsmehr im Wege steht.

Page 318: Scala : objektfunktionale Programmierung

Schlusswort

Das ist sie also: meine Einführung in Scala. Ich hoffe, Sie hatten beim Lesen ge-nauso viel Spaß wie ich beim Ausdenken, Schreiben und Programmieren. Das,was ich Ihnen in diesem Buch gezeigt habe, bezieht sich auf die im Oktober 2010aktuellen Versionen von Scala, den Tools und den Frameworks. Obwohl sich inder Entwicklung sehr viel bewegt, bleibt der Inhalt sicher lange nützlich undaktuell, und es kommen nur neue Features hinzu. So wird Scala z.B. mit Ver-sion 2.9 parallele Collections bekommen. Den auf der Website zu diesem Buchhttp://scala.obraun.net/ verlinkten Code werde ich jeweils an neue Ver-sionen anpassen.Die Scala-Welt und das Universum der funktionalen Programmierung haben abernoch sehr viel mehr zu bieten, als ich auf den vorangegangenen Seiten präsentie-ren konnte. So implementiert das Scalaz-Framework5 weitere Konzepte der funk-tionalen Programmierung, und viele Scala-Enthusiasten wagen mindestens einenBlick in Richtung einer rein funktionalen Sprache wie etwa Haskell.Sollten Sie noch Fragen oder Anmerkungen haben, zögern Sie nicht, mir [email protected] zu schreiben. Wenn Sie noch weiteren Informationsbedarfhaben oder Ihren Chef oder Auftraggeber überzeugen wollen, auf Scala umzu-steigen, versuche ich gerne, Ihnen dabei zu helfen. Ich würde mich freuen, vonIhnen zu hören.

5 http://code.google.com/p/scalaz/

Page 319: Scala : objektfunktionale Programmierung
Page 320: Scala : objektfunktionale Programmierung

Literaturverzeichnis

[Agh86] AGHA, Gul: ACTORS: A Model of Concurrent Computation in DistributedSystems, MIT, Diss., 1986

[Arm07] ARMSTRONG, Joe: Programming Erlang: Software for a Concurrent World.Pragmatic Programmers, 2007

[Ass05] ASSISI, Ramin: Eclipse 3. Carl Hanser Verlag, 2005

[Bir98] BIRD, Richard: Introduction to Functional Programming using Haskell.Prentice Hall, 1998

[BOSW98] BRACHA, Gilad; ODERSKY, Martin; STOUTAMIRE, David; WADLER,Philip: GJ Specification. 1998

[BS07] BEUST, Cedric; SULEIMAN, Hani: Next Generation Java Testing: TestNGand Advanced Concepts. Addison-Wesley Longman, 2007

[Bö08] BÖCK, Heiko: NetBeans Platform 6 - Rich-Client-Entwicklung mit Java.Galileo Computing, 2008

[CBWD09] CHEN-BECKER, Derek; WEIR, Tyler; DANCIU, Marius: The DefinitiveGuide to Lift: A Scala-based Web Framework. Apress, 2009

[CGLO06] CREMET, Vincent; GARILLOT, François; LENGLET, Sergueï; ODERSKY,Martin: A Core Calculus for Scala Type Checking. In: Proceedings ofMFCS 06, Stará Lesná (2006)

[Cli81] CLINGER, William: Foundations of Actor Semantic, MIT, Diss., 1981

[Hew09] HEWITT, Carl: ActorScript(TM): Industrial strength integration of lo-cal and nonlocal concurrency for Client-cloud Computing. (2009)

[HMUL05] HAROLD, Elliotte R.; MEANS, W. S.; UDEMADU, Katharina; LICHTEN-BERG, Kathrin: XML in a Nutshell. O’Reilly, 2005

[Hug89] HUGHES, John: Why Functional Programming Matters. In: The Com-puter Journal (1989)

Page 321: Scala : objektfunktionale Programmierung

308 Literaturverzeichnis

[Hut92] HUTTON, Graham: Higher-order functions for parsing. In: Journal ofFunctional Programming (1992)

[Mey00] MEYER, Bertrand: Object-Oriented Software Construction. Prentice Hall,2000

[MH03] MONIN, Jean F.; HINCHEY, Michael G.: Understanding formal methods.Springer, 2003

[NW06] NAFTALIN, Maurice; WADLER, Philip: Java Generics and Collections.O’Reilly, 2006

[Ode00] ODERSKY, Martin: Programming With Functional Nets / École Poly-technique Fédérale de Lausanne. 2000. – Forschungsbericht

[Ode10a] ODERSKY, Martin: A Postfunctional Language.http://www.scala-lang.org/node/4960, Januar 2010

[Ode10b] ODERSKY, Martin: The Scala Language Specification.http://www.scala-lang.org/, 2010

[Oka99] OKASAKI, Chris: Purely Functional Data Structures. Cambridge Uni-versity Press, 1999

[OSG08] O’SULLIVAN, Bryan; STEWART, Donald B.; GOERZEN, John: RealWorld Haskell. O’Reilly, 2008

[OSV08] ODERSKY, Martin; SPOON, Lex; VENNERS, Bill: Programming in Scala:A comprehensive step-by-step guide. Artima, 2008

[OW97] ODERSKY, Martin; WADLER, Philip: Pizza into Java: Translating theo-ry into practice. In: Proc. 24th ACM Symposium on Principles of Program-ming Languages, Paris, France (1997)

[PH06] PEPPER, Peter; HOFSTEDT, Petra: Funktionale Programmierung: Sprach-design und Programmiertechnik. Springer, 2006

[Pie02] PIERCE, Benjamin: Types and Programming Languages. MIT Press, 2002

[Son08] SONATYPE: Maven: The Definitive Guide. O’Reilly, 2008

[ST95] SHAVIT, Nir; TOUITOU, Dan: Software Transactional Memory. In: Pro-ceedings of the 14th ACM Symposium on Principles of Distributed Compu-ting (1995)

[Tan09] TANENBAUM, Andrew S.: Moderne Betriebssysteme. Pearson Studium,2009

Page 322: Scala : objektfunktionale Programmierung

Literaturverzeichnis 309

[TJJV04] TRUYEN, Eddy; JOOSEN, Wouter; JØRGENSEN, Bo N.; VERBAETEN, Pi-erre: A Generalization and Solution to the Common Ancestor Dilem-ma Problem in Delegation-Based Object Systems. In: Proceedings of the2004 Dynamic Aspects Workshop (2004)

[TLMG10] TAHCHIEV, Petar; LEME, Felipe; MASSOL, Vincent; GREGORY, Gary:JUnit in Action. Manning, 2010

[WBB10] WEBER, Bernd; BAUMGARTNER, Patrick; BRAUN, Oliver: OSGi fürPraktiker. Carl Hanser Verlag, 2010

Page 323: Scala : objektfunktionale Programmierung
Page 324: Scala : objektfunktionale Programmierung

Stichwortverzeichnis

Actor! 197Act 196DaemonActor 207Reactor 208RemoteActor 211ReplyReactor 208TIMEOUT 198actor 194alive 211loopWhile 205loop 205reactWithin 203react 203receiveWithin 198receive 197register 211remoteActor 212self 197Scheduler 209

Annotation 44deprecated 44serializable 44spezialized 157tailrec 107throws 128transient 44unchecked 44, 124volatile 44Self-Type 160Varianz-Annotation 150

ArgumentDefault 36Named 36

Array 29args 40

Bedarfsauswertung 102Bezeichner 31

Call-by-Name 140Call-by-Value 139Closure 104Collections 170Context Bound 155Currysierung 132

dynamisches Binden 72

Eclipse 22Enumeration 65Evaluation

Eager 102Lazy 102

Exception Handling 126Extraktor 128

Feld 47Filter 142For-Comprehension 141For-Expression 141Funktion 35, 104dropWhile 112exists 112

Page 325: Scala : objektfunktionale Programmierung

312 Stichwortverzeichnis

filterNot 112filter 112flatMap 144foldLeft 112foldRight 112forall 112foreach 111map 110parseAll 183partition 112reduceLeft 113reduceRight 113scanLeft 113scanRight 113span 112takeWhile 112Angabe des Ergebnis-

typs notwendig 36Closure 104endrekursiv 106höherer Ordnung 108ineinander verschachtelte 37Literal 104Parameterliste 35partiell angewendete 110partielle 124rekursive 104tail recursiv 106variable Parameteranzahl 37

Gültigkeitsbereich 41Generator 41, 141

Higher-Order-Function 108

IDEIntelliJ IDEA 24NetBeans 23Scala-IDE for Eclipse 22

Implicits 95Import Selector Clause 81IntelliJ IDEA 24Interface

Rich 83

Java-Integration 29

Klasse-Parameter 62AnyRef 167AnyVal 167Any 165Array 29, 155ArrowAssoc 98, 169Future 207ListBuffer 30List 28, 111NodeSeq 178ScalaObject 167Set 34TailCalls 107abtrakte 73Basis- 67Case 119leerer Rumpf 70Sub- 67Super- 67versiegelte 122

Kommandofsc 10, 15javap 15mvn 17sbaz 10sbt 19scalac 10, 13scaladoc 10, 16scalap 10, 15scala 9, 11

Kommentar 33Konstruktor 62

auxiliary 63primärer 62zusätzlicher 63

Kontrollstrukturdo-while 40for 41if-else 40while 40eigene 135

Page 326: Scala : objektfunktionale Programmierung

Stichwortverzeichnis 313

Manifest 156Maven-Scala-Plugin 17Member 47

abtraktes 73Methode 47

Überladen 55!= 166<fieldname>_= 50+= 30-> 169== 166andThen 125apply 28, 59asInstanceOf 167assert 169assume 169copy 121curried 133equals 166eq 168error 169exit 169foreach 172hashCode 166isDefinedAt 125isInstanceOf 167iterator 172lift 125main 43ne 168orElse 125println 170readLine 170require 169r 131toString 166unapply 120unary_<op> 59update 29, 59empty-paren 58Extraktor- 120Factory- 64Getter 50parameterless 58

Redefinition 50Setter 50

Mixin 84

Nebenläufigkeit 193NetBeans 23

ObjektActor 194Predef 169Companion- 53

Operator<:< 158<%< 158+= 30/: 113:: 30:> 151:\ 113<: 151<% 154=:= 158Assoziativität 32auf : endend 31auf = endend 30Cons 30Infix 31, 59Postfix 40, 59Präfix 59Priorität 32zulässige Zeichen 32

Package 78Package Object 79Packages

nested 78Parameter

By-Name- 140implizite 98variable Anzahl 37

Parameterlistegeschweifte Klammern 135

Parser 180Packrat 181

partially applied function 110

Page 327: Scala : objektfunktionale Programmierung

314 Stichwortverzeichnis

PatternCase-Klassen 119Extraktoren 128regulärer Ausdruck 130Teil- 126Typ 116Variable 116Wildcard 115XML 179

Pattern Matching 114Guards 118

Polymorphie 72Polymorphismus

parametrischer 148Predef-Objekt 169Property 52Prozedur 35

Rekursion 104REPL 10, 11Rich-Wrapper 95

Sbt 19Schlüsselwort 33abstract 75case 114, 119catch 126class 47def 35else 40extends 67finally 127final 70for 41, 141if 40implicit 95import 80lazy 102match 114new 28, 49, 62object 47override 50package 78private 56protected 56

return 35sealed 122super 69this 63trait 83try 126type 39, 60val 28var 28while 40with 85yield 143

Scope 41Script 39Self-Type-Annotation 160Semikolon-Inferenz 27, 38Sichtbarkeitsmodifier 56private 56protected 56public 56Companion-Objekt 57

Simple Build Tool 19

Thread 194Trait 82Actor 194Application 42, 65BitSet 175ClassManifest 156Functionn 104Iterable 172JavaTokenParsers 181LinearSeq 173Map 175Ordered 87PartialFunction 124RegexParsers 181Seq 173Set 174SortedMap 176SortedSet 175Traversable 172Vector 173Linearisierung 94

Page 328: Scala : objektfunktionale Programmierung

Stichwortverzeichnis 315

Stackable 88Trampoline 107Tupel 33

Zuweisung an 34TypAnyRef 147AnyVal 147Any 147Nothing 147Null 147Option 147ScalaObject 147Unit 35_* 37Bottom- 147Boxing 157Context Bound 155existenzieller 162Generalisierung 68Generalized Type

Constraints 158Inferenz 147Infix-Schreibweise 148Konstruktor 148Lower Bound 151parametrischer 148parametrisierter 28Spezialisierung 68, 157struktureller 162Sub- 68Subtyp 149Super- 68Synonym 39, 60Upper Bound 151

View Bound 154Typinferenz 27Typisierung 27, 147

UmgebungsvariableJAVACMD 10JAVA_HOME 11JAVA_OPTS 11

Umwandlungimplizite 95

Uniform Access Principle 58

VariableInstanz- 49unveränderliche 28veränderliche 28

VarianzAnnotation 150kontravariant 150kovariant 150nonvariant 149

Vererbung 67View 154View Bound 154

XML 176Brace Escapes 177Literal 176Pattern Matching 179

Zeichenkettemehrzeilig 33

Zuweisunggleichzeitig an mehrere

Bezeichner 34

Page 329: Scala : objektfunktionale Programmierung

Mehr Informationen zu diesem Buch und zu unserem Programmunter www.hanser.de/computer

Dieses Praxisbuch führt Sie in die Welt von OSGi ein und vermittelt Ihnen die wich-

tigsten Prinzipien, um OSGi erfolgreich in Ihrem Projekt einzusetzen. Es unterstützt

Sie bei der Auswahl der Werkzeuge und der geeigneten Realisierungsvarianten und

nicht zuletzt bei der Migration bestehender Applikationen.

Die Themen reichen von der Beschreibung des zentralen bnd-Tools von Peter Kriens

über die Kollaboration des Spring Frameworks mit OSGi bis zur Betriebsüberwachung

mittels JMX, von verteilten OSGi-Systemen bis zur OSGi Webapplikation mit Web

Bundles. Der Build-Prozess wird ausführlich beschrieben, da er eine zentrale Rolle in

der Software-Erstellung einnimmt und enormes Effektivitäts-Potential hat. Und natür-

lich kommt auch das Testen nicht zu kurz. Egal, ob Sie OSGi erst kennen lernen oder

Ihr OSGi-Know-how vertiefen wollen, Sie finden in diesem Buch aktuelle und kompe-

tente Informationen, die Sie direkt in Ihrem Projekt anwenden können.

Weber, Baumgartner, Braun

OSGi für Praktiker

Prinzipien, Werkzeuge und praktische

Anleitungen auf dem Weg zur kleinen SOA

298 Seiten

ISBN 978-3-446-42094-6

OSGi erfolgreich einsetzen

Page 330: Scala : objektfunktionale Programmierung

DAS UMFASSENDE BUCH ZUR PROGRAMMIERUNG IN SCALA //

■ Entdecken Sie die ganze Welt von Scala: Die Sprache, die Tools, die

Bibliotheken, die Frameworks

■ Nutzen Sie die nachvollziehbaren Beispiele, um in der Scala-Programmierung

rasch fit zu werden.

■ Im Internet: Der Quellcode aus dem Buch zum Download unter

http://scala.obraun.net/

SCALA // Sie sind neugierig auf Scala, die neue, universale Programmiersprache,

die Objektorientierung und funktionale Programmierung auf einzigartige Weise ver-

eint? Sie wollen diese Sprache kennenlernen und erfahren, welche Vorzüge sie

hat? Dann sind Sie hier richtig.

Oliver Braun führt Sie detailliert in die Sprache und ihre Syntax ein und stellt die

Arbeitsumgebung für die Programmierung von Scala und die Ausführung des Codes

vor. Er geht auf die für die Praxis wichtigen Themen Dokumentation und Testen ein

und macht Sie mit den wichtigsten Bibliotheken und Frameworks wie Lift und Akka

vertraut. Besonders stark ist Scala im Bereich der nebenläufigen Programmierung,

daher ist auch diesem Thema ein eigenes Kapitel gewidmet.

Insgesamt erhalten Sie einen umfassenden Überblick, welche Vorzüge Scala in der

Praxis tatsächlich hat, und eignen sich die Sprache anhand der vielen nachvollzieh-

baren Beispiele rasch an.

Prof. Dr. Oliver BRAUN ist Professor für Programmierung und verteilte Systeme an

der Fachhochschule Schmalkalden. Zu seinen Schwerpunkten zählt die Funktionale

Programmierung. Neben seiner Hochschultätigkeit berät er Firmen zu gesamtheit -

lichen IuK-Konzepten.

www.hanser.de/computer

Softwareentwickler, Software -

architekten, Programmierer

ISBN 978-3-446-42399-2

Sys

tem

vora

usse

tzun

gen

für

eBoo

k-in

sid

e: In

tern

et-V

erb

ind

ung

und

eB

ookr

ead

er A

dob

e D

igita

l Ed

ition

s.

9 783446 423992

AUS DEM INHALT // Reine Objektorientierung // Funktionales Programmieren //

Die Scala-Standardbibliothek // Actors – Concurrency und Multicore-Programmierung //

Softwarequalität – Dokumentieren und Testen // Webprogrammierung mit Lift //

Leichtgewichtige Webprogrammierung mit Scalatra // Akka – Actors und Software

Transactional Memory //