oliver BRAUN
SCALA OBJEKTFUNKTIONALE PROGRAMMIERUNG
Braun
Scalaȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ ȱ
v
Bleiben Sie einfach auf dem Laufenden: www.hanser.de/newsletter Sofort anmelden und Monat für Monat die neuesten Infos und Updates erhalten. ȱ
OliverȱBraunȱ
Scalaȱ Objektfunktionale Programmierungȱ
Prof.ȱDr.ȱOliverȱBraun,ȱMünchenȱ
[email protected]ȱ ȱ ȱ ȱ ȱ Alleȱ inȱ diesemȱ Buchȱ enthaltenenȱ Informationen,ȱ Verfahrenȱ undȱ Darstellungenȱ wurdenȱ nachȱ bestemȱWissenȱzusammengestelltȱundȱmitȱSorgfaltȱgetestet.ȱDennochȱsindȱFehlerȱnichtȱganzȱausȬ zuschließen.ȱ Ausȱ diesemȱ Grundȱ sindȱ dieȱ imȱ vorliegendenȱ Buchȱ enthaltenenȱ Informationenȱ mitȱ keinerȱVerpflichtungȱoderȱGarantieȱirgendeinerȱArtȱverbunden.ȱAutorȱundȱVerlagȱübernehmenȱ infolgedessenȱkeineȱjuristischeȱVerantwortungȱundȱwerdenȱkeineȱdarausȱfolgendeȱoderȱsonstigeȱ Haftungȱ übernehmen,ȱ dieȱ aufȱ irgendeineȱ Artȱ ausȱ derȱ Benutzungȱ dieserȱ Informationenȱ –ȱ oderȱ Teilenȱdavonȱ–ȱentsteht,ȱauchȱnichtȱfürȱdieȱVerletzungȱvonȱPatentrechtenȱundȱanderenȱRechtenȱ Dritter,ȱdieȱdarausȱresultierenȱkönnten.ȱAutorȱundȱVerlagȱübernehmenȱdeshalbȱkeineȱGewährȱ dafür,ȱdassȱdieȱbeschriebenenȱVerfahrenȱfreiȱvonȱSchutzrechtenȱDritterȱsind.ȱ Dieȱ Wiedergabeȱ vonȱ Gebrauchsnamen,ȱ Handelsnamen,ȱ Warenbezeichnungenȱ usw.ȱ inȱ diesemȱ Buchȱ berechtigtȱ deshalbȱ auchȱ ohneȱ besondereȱ Kennzeichnungȱ nichtȱ zuȱ derȱ Annahme,ȱ dassȱ solcheȱNamenȱimȱSinneȱderȱWarenzeichenȬȱundȱMarkenschutzȬGesetzgebungȱalsȱfreiȱzuȱbetrachȬ tenȱwärenȱundȱdaherȱvonȱjedermannȱbenutztȱwerdenȱdürften.ȱ ȱ ȱ ȱ ȱ BibliografischeȱInformationȱderȱDeutschenȱNationalbibliothek:ȱ Dieȱ Deutscheȱ Nationalbibliothekȱ verzeichnetȱ dieseȱ Publikationȱ inȱ derȱ Deutschenȱ NationalȬ bibliografie;ȱ detaillierteȱ bibliografischeȱ Datenȱ sindȱ imȱ Internetȱ überȱ http://dnb.ddb.deȱ abrufȬ bar.ȱ ȱ ȱ 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ȱdesȱ VerlagesȱinȱirgendeinerȱFormȱ(Fotokopie,ȱMikrofilmȱoderȱeinȱanderesȱVerfahren)ȱ–ȱauchȱnichtȱfürȱ ZweckeȱderȱUnterrichtsgestaltungȱ–ȱreproduziertȱoderȱunterȱVerwendungȱelektronischerȱSysȬ temeȱverarbeitet,ȱvervielfältigtȱoderȱverbreitetȱwerden.ȱ ȱ ©ȱ2011ȱCarlȱHanserȱVerlagȱMünchenȱ(www.hanser.de)ȱ ȱ Lektorat:ȱMargareteȱMetzgerȱ Herstellung:ȱIreneȱWeilhartȱ ȱ Copyȱediting:ȱJürgenȱDubau,ȱFreiburg/Elbeȱ Umschlagdesign:ȱMarcȱMüllerȬBremer,ȱwww.rebranding.de,ȱMünchenȱ Umschlagrealisation:ȱStephanȱRönigkȱ Datenbelichtung,ȱDruckȱundȱBindung:ȱKösel,ȱKrugzellȱ ȱ
Ausstattungȱpatentrechtlichȱgeschützt.ȱKöselȱFDȱ351,ȱPatentȬNr.ȱ0748702 PrintedȱinȱGermanyȱ ISBNȱ978Ȭ3Ȭ446Ȭ42399Ȭ2ȱ
für Mia
Inhaltsverzeichnis Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
XI
1
Einführung . . . . . . . . . . . . . . . . . . . . . . . 1.1 Was Führungskräfte über Scala wissen sollten 1.2 Java-Scala-Integration . . . . . . . . . . . . . . 1.3 Über dieses Buch . . . . . . . . . . . . . . . . . 1.4 Typographische und sonstige Konventionen .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
1 3 4 5 6
2
Einrichten der Arbeitsumgebung . . . . . . 2.1 Die Scala-Shell und die Kommandozeile 2.1.1 Der Scala-Interpreter . . . . . . . 2.1.2 Die Scala-(De-)Compiler . . . . . 2.1.3 Der Dokumentationsgenerator . 2.2 Buildtools . . . . . . . . . . . . . . . . . 2.2.1 Das Maven-Scala-Plugin . . . . . 2.2.2 Simple Build Tool . . . . . . . . . 2.3 IDE-Support . . . . . . . . . . . . . . . . 2.3.1 Eclipse . . . . . . . . . . . . . . . 2.3.2 NetBeans . . . . . . . . . . . . . . 2.3.3 IntelliJ IDEA . . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
9 9 11 13 16 17 17 19 22 22 23 24
3
Grundlagen . . . . . . . . . . . . . 3.1 Ein kleines bisschen Syntax . 3.2 Imperative Programmierung 3.3 Ein ausführbares Programm . 3.4 Annotations . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
27 27 39 42 44
4
Reine Objektorientierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1 Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . .
47 47
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
VIII
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
47 60 62 65 67 73 78 78 79 80 82 83 88 95
Funktionales Programmieren . . . . . . . . . . . . . . . . . 5.1 Lazy Evaluation . . . . . . . . . . . . . . . . . . . . . . 5.2 Funktionen und Rekursionen . . . . . . . . . . . . . . 5.3 Higher-Order-Functions . . . . . . . . . . . . . . . . . 5.4 Case-Klassen und Pattern Matching . . . . . . . . . . 5.4.1 Case-Klassen . . . . . . . . . . . . . . . . . . . 5.4.2 Versiegelte Klassen . . . . . . . . . . . . . . . . 5.4.3 Partielle Funktionen . . . . . . . . . . . . . . . 5.4.4 Variablennamen für (Teil-)Pattern . . . . . . . 5.4.5 Exception Handling . . . . . . . . . . . . . . . 5.4.6 Extraktoren . . . . . . . . . . . . . . . . . . . . 5.4.7 Pattern Matching mit regulären Ausdrücken . 5.5 Currysierung und eigene Kontrollstrukturen . . . . . 5.6 For-Expressions . . . . . . . . . . . . . . . . . . . . . . 5.7 Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . 5.7.1 Standardtypen . . . . . . . . . . . . . . . . . . 5.7.2 Parametrischer Polymorphismus und Varianz 5.7.3 Upper und Lower Bounds . . . . . . . . . . . . 5.7.4 Views und View Bounds . . . . . . . . . . . . . 5.7.5 Context Bounds . . . . . . . . . . . . . . . . . . 5.7.6 Arrays und @specialized . . . . . . . . . . . . 5.7.7 Generalized Type Constraints . . . . . . . . . . 5.7.8 Self-Type-Annotation . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
101 102 104 108 114 119 122 124 126 126 128 130 132 141 147 147 148 151 154 155 155 158 160
4.2
4.3
4.4 5
Inhaltsverzeichnis
4.1.1 Felder und Methoden . . . . . . . . . . . 4.1.2 Was Klassen sonst noch enthalten können 4.1.3 Konstruktoren . . . . . . . . . . . . . . . . 4.1.4 Enumerations . . . . . . . . . . . . . . . . 4.1.5 Vererbung und Subtyping . . . . . . . . . 4.1.6 Abstrakte Klassen . . . . . . . . . . . . . Codeorganisation . . . . . . . . . . . . . . . . . . 4.2.1 Packages . . . . . . . . . . . . . . . . . . . 4.2.2 Package Objects . . . . . . . . . . . . . . . 4.2.3 Importe . . . . . . . . . . . . . . . . . . . Traits . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.1 Rich Interfaces . . . . . . . . . . . . . . . 4.3.2 Stapelbare Modifikationen . . . . . . . . . Implicits und Rich-Wrapper . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
Inhaltsverzeichnis
5.7.9
IX
Strukturelle und existenzielle Typen . . . . . . . . . . . . . . 162
6
Die Scala-Standardbibliothek . . . . . 6.1 Überblick und das Predef-Objekt 6.2 Das Collection-Framework . . . . . 6.3 Scala und XML . . . . . . . . . . . 6.4 Parser kombinieren . . . . . . . . . 6.5 Ein kleines bisschen GUI . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
165 165 170 176 180 187
7
Actors – Concurrency und Multicore-Programmierung . 7.1 Ein Thread ist ein Actor . . . . . . . . . . . . . . . . 7.2 Empfangen und Reagieren . . . . . . . . . . . . . . . 7.3 Dämonen und Reaktoren . . . . . . . . . . . . . . . . 7.4 Scheduler . . . . . . . . . . . . . . . . . . . . . . . . . 7.5 Remote Actors . . . . . . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
193 194 196 207 209 211
8
Softwarequalität – Dokumentieren und Testen . . . . . . 8.1 Scaladoc . . . . . . . . . . . . . . . . . . . . . . . . . . 8.2 ScalaCheck . . . . . . . . . . . . . . . . . . . . . . . . . 8.2.1 Grundlagen . . . . . . . . . . . . . . . . . . . . 8.2.2 Generatoren . . . . . . . . . . . . . . . . . . . . 8.2.3 Automatisiertes Testen mit Sbt . . . . . . . . . 8.3 ScalaTest . . . . . . . . . . . . . . . . . . . . . . . . . . 8.3.1 ScalaTest und JUnit . . . . . . . . . . . . . . . . 8.3.2 ScalaTest und TestNG . . . . . . . . . . . . . . 8.3.3 ScalaTest und BDD . . . . . . . . . . . . . . . . 8.3.4 Funktionale, Integrations- und Akzeptanztests 8.3.5 Die FunSuite . . . . . . . . . . . . . . . . . . . 8.4 Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.4.1 Eine Specs-Spezifikation . . . . . . . . . . . . . 8.4.2 Matchers . . . . . . . . . . . . . . . . . . . . . . 8.4.3 Mocks mit Mockito . . . . . . . . . . . . . . . . 8.4.4 Literate Specifications . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
215 216 221 221 224 229 232 233 235 236 238 240 241 242 244 248 249
9
Webprogrammierung mit Lift . . . . . . 9.1 Quickstart mit Lift . . . . . . . . . . 9.2 Bootstrapping . . . . . . . . . . . . . 9.3 Rendering – Templates und Snippets 9.4 Benutzerverwaltung und SiteMap .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
253 254 257 262 264
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . . .
. . . . .
. . . . .
. . . . .
X
Inhaltsverzeichnis
9.5 9.6
Persistenz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 Implementierung der Snippets . . . . . . . . . . . . . . . . . . . . . 269
10 Leichtgewichtige Webprogrammierung mit Scalatra . . . . . . . . . . . 279 10.1 Quickstart mit Scalatra . . . . . . . . . . . . . . . . . . . . . . . . . . 279 10.2 Der Final-Grade-Calculator . . . . . . . . . . . . . . . . . . . . . . . 281 11 Akka – Actors und Software Transactional Memory 11.1 Quickstart mit Akka . . . . . . . . . . . . . . . . 11.2 Der MovieStore . . . . . . . . . . . . . . . . . . . 11.3 User- und Session-Management . . . . . . . . . . 11.4 Software Transactional Memory . . . . . . . . . . 11.5 Client und Service . . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
287 288 289 293 297 300
Schlusswort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305 Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309 Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
Vorwort Scala: Eine Programmiersprache, die auf einzigartige Weise die objektorientierte Programmierung mit der funktionalen1 verschmilzt, die sich anschickt, Java vom Thron zu stoßen, und mit der es richtig Spaß macht zu programmieren. Im Sommersemester 2010 habe ich erstmals neben Haskell auch Scala in der Vorlesung „Fortgeschrittene funktionale Programmierung” vorgestellt und viele erfreuliche Erfahrungen gesammelt. Ich erlebte, wie meine Studierenden die vermittelten funktionalen Konzepte mit Scala sehr gut auf die JVM übertragen konnten. Ab dem Wintersemester 2010/11 begann ich damit, Scala statt Java für die Programmierausbildung der Studienanfänger zu verwenden. In Scala sind viele Dinge einfacher und sauberer umgesetzt worden. Beispielsweise lässt sich der Ausdruck println("Hello World") ganz alleine in einer Datei als Script ausführen oder direkt in den interaktiven Scala-Interpreter eintippen. In Scala können wir dann zunächst so nah an Java bleiben, dass ein späterer Umstieg kaum Probleme bereiten dürfte2 . Dass ich Scala auch für die Programmierung verteilter Systeme 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 wesentlichen Tools und Frameworks vorstellen werde. Mein herzlichster Dank gilt allen, die dieses Buch ermöglicht und mich in diesem Projekt unterstützt haben: Allen voran meine Familie. Wichtige Diskussionen und Anmerkungen haben Patrick Baumgartner, Jürgen Dubau, Christoph Schmidt, Heiko Seeberger und Bernd Weber beigesteuert. Vielen Dank dafür. Für die gute Zusammenarbeit mit dem Hanser Verlag danke ich stellvertretend Margarete Metzger und Irene Weilhart. Happy Scala Hacking wünscht Oliver Braun München, Oktober 2010 1
Dass sich die funktionale Programmierung langsam aber sicher in den Mainstream vorarbeitet, muss ich hier sicher nicht erwähnen. 2 . . . und sobald wir die weitergehenden Konzepte besprechen, will sowieso keiner mehr von Scala weg ;-).
Kapitel 1
Einführung Schon wieder eine neue Programmiersprache, obwohl: Scala ist gar nicht mehr so neu. Die Entwicklung begann 2001 an der École polytechnique fédérale de Lausanne (EPFL) in der Schweiz von einem Team um Professor Martin Odersky. Das erste Release wurde bereits 2003 veröffentlicht. 2006 folgte die Version 2.0 und 2010, gerade während dieses Buch geschrieben wird, wird die Version 2.8 freigegeben, die laut Odersky eigentlich 3.01 heißen müsste. Professor Odersky ist in der Java-Welt kein Unbekannter. 1995 begann er mit Philip Wadler2 mit der Entwicklung der funktionalen Programmiersprache Pizza3 , die sich in Bytecode für die Java Virtual Machine (JVM) übersetzen lässt. Diese Arbeit führte über GJ4 schließlich zum neuen javac-Compiler und den mit Java 5 eingeführten Java Generics5 . Als Odersky 1999 an die EPFL kam, verschob er seinen Fokus etwas. Er hatte immer noch das Ziel, die objektorientierte und funktionale Programmierung zu verbinden, wollte sich aber nicht mehr mit den Restriktionen von Java belasten. Nach der Entwicklung der Programmiersprache Funnel6 nahm er als weitere Ziele die praktische Verwendbarkeit und die Interoperabilität mit Standardplattformen hinzu und entwarf Scala7 . Scala ist eine Hybrid-Sprache, die auf einzigartige Weise Features von objektorientierten und funktionalen Programmiersprachen verbindet. Designziel von Scala 1 Eigentlich sollte nur das Collection-Framework (siehe Abschnitt 6.2) einem Redesign unterworfen werden. Die Arbeiten daran dauerten aber etwa 18 Monate, sodass parallel dazu eine Vielzahl anderer Features entstanden, die in die bereits angekündigte Version 2.8 aufgenommen wurden. Von der bereits 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/
2
1 Einführung
ist eine knappe, elegante und typsichere Programmierung. Scala wird nicht nur in Bytecode für die JVM kompiliert, es lässt sich auch jeglicher Java-Code direkt aus Scala heraus nutzen und umgekehrt. Scala ist eine rein objektorientierte Programmiersprache. Das heißt, in Scala ist jeder Wert ein Objekt. Scala nutzt ein Konzept von Klassen und Traits. Mit Traits lassen sich Rich-Interfaces realisieren, denn Traits können bereits konkrete Implementierungen enthalten. Klassen werden durch Vererbung erweitert, Traits werden in eine Klasse oder in ein Objekt hineingemixt. Um nicht in typische Probleme mit Mehrfachvererbung zu laufen, werden Traits linearisiert. Ob Scala sich funktionale Programmiersprache nennen darf, wurde im Web kürzlich erst ausführlich diskutiert. Odersky bezeichnet Scala schließlich in [Ode10a] als postfunktionale Sprache. Fest steht auf alle Fälle, dass Scala über eine Reihe von Features verfügt, die entweder der funktionalen Programmierung zuzurechnen sind oder aus ihrem Umfeld entstammen. In Scala ist jede Funktion ein Wert8 und kann gleichberechtigt mit anderen Werten behandelt werden. Das bedeutet beispielsweise, eine Funktion kann Argument oder Ergebnis einer anderen Funktion9 sein, Funktionen können in Listen gespeichert werden, und Funktionen können ineinander verschachtelt werden. Darüber hinaus unterstützt Scala auch Features wie Pattern Matching10 und Currysierung11 . Im Gegensatz zu vielen modernen und angesagten Programmiersprachen ist Scala 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 Sprachen der Fall ist. Nachdem eine überwiegende Anzahl von Programmierfehlern Typfehler sind, ist die statische Typisierung unserer Ansicht nach im allgemeinen vorzuziehen. Dem wesentlichen Nachteil, nämlich der Notwendigkeit, überall Typen angeben zu müssen, wird in Scala mit einem Typinferenzmechanismus begegnet. 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 und polymorphen Methoden auch Varianz-Annotationen, Upper und Lower Bounds und vieles mehr zur Verfügung stellt. Ein weiteres Merkmal von Scala, was übrigens für Scalable Language steht, ist die einfache Erweiterbarkeit. Damit ist Scala prädestiniert zur Erstellung von Domain Specific Languages (DSLs). Zu guter Letzt kann Scala auch noch mit Unterstützung für die .NET-Plattform aufwarten. Dabei handelt es sich allerdings noch nicht um einen Stand, der als „production ready” zu bezeichnen ist. 8
und damit ein Objekt Funktionen, die eine Funktion als Argument oder Ergebnis haben, heißen Higher Order Functions, zu deutsch: Funktionen höherer Ordnung. 10 siehe Abschnitt 5.4 11 siehe Abschnitt 5.5 9
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 Scala ist sehr aktiv, von Bugfix-Releases über neue Features wird uns in der nächsten Zeit noch einiges erwarten. Für die kommenden Jahre steht der Fokus des ScalaTeams 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önnen 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 nutzen. Zusammenfassend heißt das: Scala kann gefahrlos ausprobiert werden. Selbst wenn zu Java zurückgewechselt werden sollte, ist die Arbeit in Scala nicht umsonst. Und die gesamte Werkzeugpalette, die für die Java-Entwicklung genutzt wird, wie z.B. Eclipse, NetBeans oder Maven, wird zur Entwicklung von ScalaCode einfach weiter benutzt. Warum aber sollten Sie überhaupt zu Scala wechseln? Scala erhöht die Produktivität! Scala bietet eine Vielzahl von Features, mit denen Sie kürzeren und eleganteren Code schreiben können. Sicherlich ist die bloße Anzahl von Lines of Code nicht sehr aussagekräftig, aber mit weniger Code gibt es mindestens statistisch gesehen auch weniger Fehler. Scala ist strenger typisiert als Java. Auch dadurch schlupfen weniger Fehler durch. Und ein wesentlicher Vorteil beim Start mit Scala ist, dass es sich fast wie Java anfühlt und zusätzliche Features nach und nach eingeflochten werden können. Warum denn Scala und nicht eine der unzähligen anderen Sprachen? Dazu möchten 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 other language on the JVM seems as capable of being a “replacement for Java” as Scala, and the momentum behind Scala is now unquestionable. While Scala is not a dynamic language, it has many of the characteristics of popular dynamic languages, through its rich and flexible type system, its sparse and clean syntax, and its marriage 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 very impressed with it! I can honestly say if someone had shown me the Programming
4
1 Einführung
in Scala book by Martin Odersky, Lex Spoon & Bill Venners back in 2003 I’d probably 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 Erfolgsgeschichten: 1. Électricité de France Trading (EDFT) ist eine Tochterfirma von Frankreichs größter Energiefirma EDF, die sich mit dem Energiemarkt befasst. In den letzten Jahren hat EDFT einen substanziellen Teil der 300.000 Zeilen Java-Code für „Trading and Pricing” erfolgreich durch Scala ersetzt. EDFT spricht von einer signifikanten Steigerung der Produktivität und von sehr viel verbesserten Schnittstellen für ihre Händler. Der Teamleiter Alex McGuire hat mittlerweile EDFT verlassen und eine eigene Firma gegründet, die Scala-Consulting für Finanzdienstleister und Handelsunternehmen bietet. 2. Twitter12 bietet einen sehr populären Realtime-Messaging-Service, den weltweit über 70 Millionen User nutzen. Pro Tag verarbeitet die Twitter-Infrastruktur, die mittlerweile im Backend zum Großteil aus Scala-Code besteht, über 50 Millionen Kurznachrichten, sogenannte Tweets. Und was ist mit kommerziellem Support? Den gibt es bereits in verschiedenster Form. Es gibt eine sehr aktive Scala-Community und erste, auch deutsche, Firmen, die Scala-Consulting leisten. Ein wesentlicher Schritt war die Gründung der Firma ScalaSolutions durch Martin Odersky selbst. Die Firma bietet Scala-Support, -Consulting und -Training an. Und nicht zuletzt damit können interessierte und gute Java-Programmierer innerhalb kürzester Zeit zu guten Scala-Entwicklern geschult werden.
1.2
Java-Scala-Integration
Auch wenn Sie Scala noch nicht kennengelernt haben, wollen wir an dieser Stelle zur weiteren Motivation bereits etwas über die nahtlose Integration mit Java sagen. 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üssen Sie ein bisschen mehr über die Interna wissen. Scala ist, was Sprachfeatures angeht, 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/ Eine kleine Besonderheit stellen Javas Wildcard und Raw Types dar. Auch dafür gibt es in Scala eine Lösung, aber die würde an dieser Stelle etwas zu weit führen. Die Lösung, existenzielle Typen, werden wir in Abschnitt 5.7.9 besprechen. 13
1.3 Über dieses Buch
5
In Scala ist im Gegensatz zu Java alles ein Objekt. Zur besseren Performanz werden die Objekte, die in Java als primitive Typen repräsentiert werden, wenn möglich 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 Typparameter für generische Klassen zulässig sind, wird der Wert in die entsprechende Wrapper-Instanz umgewandelt. Beispielsweise wird der Scala Int in einer Liste in eine Instanz der Klasse java.lang.Integer übersetzt. Scalas reine Objektorientierung lässt keine statischen Klassenmember zu. Stattdessen hat Scala Singleton-Objekte. Aus einem Singleton-Objekt mit dem Namen MyObject 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 um ein sogenanntes Standalone-Objekt handelt, wird darüber hinaus noch eine Klasse mit dem Namen MyObject erzeugt, die statische Member für alle Member des Scala-Objekts enthält. Damit wird dann auf das Member x des Scala-Objekts MyObject in Scala wie auch in Java mit MyObject.x zugegriffen. Mit Scalas Traits ist es ein bisschen komplizierter, da Java kein entsprechendes Konstrukt kennt. Aus einem Trait wird immer ein Java-Interface und damit ein Typ erzeugt. Über Variablen dieses Typs können alle Methoden der Scala-Objekte genutzt werden. Das Implementieren eines Traits in Java ist allerdings nicht praktikabel, außer der Trait enthält nur abstrakte Member. Javas Annotations und Exceptions werden von Scala unterstützt. Spezielle ScalaAnnotations wie zum Beispiel @volatile, @transient und @serializable werden in die entsprechenden Java-Konstrukte transformiert. Scala kennt keine Checked Exceptions, bietet aber eine @throws-Annotation für die JavaInteroperabilität.
1.3
Über dieses Buch
Mit dem vorliegenden Buch wollen wir Sie in die faszinierende Welt von Scala entführen. Um Ihnen den Einstieg so angenehm wie möglich zu machen und damit Sie das von uns Beschriebene gleich praktisch ausprobieren können, beginnen wir in Kapitel 2 mit Informationen über die Scala-Tools und über die Tools zur Unterstützung des Entwicklungsprozesses. Anschließend befassen wir uns in drei Kapiteln mit der Programmiersprache Scala. In Kapitel 3 tasten wir uns an die Syntax heran und besprechen die imperativen Programmierkonzepte. Außerdem zeigen wir Ihnen, wie in Scala ausführbare Scripts entwickelt werden, und erstellen ein erstes kompilierbares Programm. Auch wenn sich die Features von Scala nicht streng in Merkmale objektorientierter und funktionaler Programmiersprachen aufteilen lassen, haben wir eine solche Aufteilung in die beiden Kapitel 4 und 5 vorgenommen. Dabei findet sich alles,
6
1 Einführung
was eher dem Bereich Objektorientierung zuzuordnen ist, in Kapitel 4, und die Features, 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 nicht nur vom Sprachkern. In zwei Kapiteln lenken wir unser Augenmerk auf die Bibliotheken, die die Scala-Distribution mitbringt. In Kapitel 6 geben wir zunächst einen groben Überblick, bevor wir einige Bereiche wie das Collection-Framework und die hervorragende XML-Unterstützung von Scala ein wenig genauer betrachten. Der Actor-Bibliothek ist aufgrund der zunehmenden Bedeutung von nebenläufiger und Multicore-Programmierung ein eigenes Kapitel gewidmet, nämlich Kapitel 7. Qualitativ hochwertige Software sollte gut dokumentiert und getestet sein. Die in der Scala-Entwicklung üblichen Ansätze stellen wir Ihnen in Kapitel 8 vor. Die letzten drei Kapitel geben Ihnen einen Einstieg in drei Scala-Frameworks. In allen drei Kapiteln entwickeln wir jeweils eine kleine Beispielapplikation. Der in Kapitel 9 erstellte Talk Allocator ist eine Web-Applikation, die auf Lift14 , einem sehr umfangreichen Web-Framework, aufsetzt. Der Final-Grade-Calculator aus Kapitel 10 nutzt das sehr leichtgewichtige Web-Framework Scalatra15 . Und schließlich tauchen wir mit dem MovieStore in Kapitel 11 ein in die faszinierende Welt von Actors und Software Transactional Memory mit Akka16 . Über die vorgestellten Frameworks hinaus gibt es eine Menge weiterer, sehr interessanter Frameworks wie beispielsweise ScalaModules, das bereits in [WBB10] beschrieben wurde. Auf der Website zum Buch http://scala.obraun.net/ finden Sie Links zum Quellcode zu einigen Kapiteln. Weitergehende Informationen zu Scala finden Sie auf der Scala-Website http://www.scala-lang.org/, auf vielen Mailinglisten, 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 Abtippen nicht mit eingegeben werden darf. Zeilen ohne das Dollarzeichen sind Ausgaben, zum Beispiel: $ scala -version Scala code runner version 2.8.0.final -- Copyright 2002-2010, LAMP/EPFL
14 15 16
siehe [CBWD09] und http://liftweb.net/ http://www.scalatra.org/ http://akkasource.org/
1.4 Typographische und sonstige Konventionen
7
Kommandos in der interaktiven Scala-Umgebung wird als Prompt scala> vorangestellt. Erstreckt sich die Eingabe über mehrere Zeilen, beginnen die folgenden Zeilen mit fünf Leerzeichen, gefolgt von einem |-Symbol. Andere Zeilen sind Ausgaben des Scala-Interpreters, beispielsweise: scala> for (i Nach dem Starten können in der Scala-Shell Ausdrücke eingegeben werden. Diese werden unmittelbar nach dem Betätigen der Return-Taste ausgewertet und das Ergebnis wird ausgegeben. Listing 2.2: Einfache Berechnungen in der Scala-Shell
scala> 1+2 res0: 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 berechnen lassen können. Als Erstes werden die Integer 1 und 2 addiert und die Summe ausgegeben. Das Ergebnis wird automatisch der Variable res0 zugewiesen und
12
2 Einrichten der Arbeitsumgebung
kann damit später wieder referenziert werden. Darüber hinaus wird der inferierte Typ ausgegeben. Das nächste Ergebnis einer Berechnung wird dann der Variablen res1 zugewiesen usw. Die drei folgenden Eingaben berechnen kein Ergebnis, sondern haben nur einen Nebeneffekt, der etwas auf der Kommandozeile ausgibt. Selbstverständlich können auch eigene Variablen eingeführt werden, und es ist möglich, Scala-Code über mehrere Zeilen einzugeben (siehe Listing 2.3). Zusätzlichen Eingabezeilen werden 5 Leerzeichen, gefolgt von einem |-Symbol, automatisch durch die Scala-Shell vorangestellt. Um eine zusätzliche Zeile zuzulassen, muss die Shell erkennen, dass es sich bei der Eingabe noch um keinen korrekten Ausdruck 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 ? @ \ ^ | ~ 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 expr2 expr3 mit drei Ausdrücken verknüpft durch zwei Operatoren als: (expr1 expr2) expr3 oder: expr1 (expr2 expr3) zu interpretieren ist. Anders gefragt: Ob stärker bindet als oder umgekehrt. Scala folgt einer einfachen Regel: Je weiter unten das Zeichen, mit dem der Operator beginnt, in Tabelle 3.1 steht, desto stärker bindet er. Zeichen in der gleichen Zeile 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 und um denselben Operator oder haben sie die gleiche Bindungsstärke, entscheidet die Assoziativität darüber, was zuerst berechnet wird. Es gilt: Die Operatoren, die mit einem : enden, sind rechtsassoziativ, alle anderen sind links-assoziativ. Das heißt zum Beispiel 1 :: 2 ::
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 unter Verwendung von Backquotes als Bezeichner genutzt werden können: abstract do finally import object requires throw val _ : =
case else for lazy override return trait var => val second = myTuple._2 second: Int = 1 scala> val third = myTuple._3 third: Boolean = true scala> val fourth = myTuple._4 fourth: Double = 5.7 Tupel können auch auf der linken Seite einer Zuweisung genutzt werden, um die verschiedenen Komponenten eines Tupels verschiedenen Bezeichnern zuzuordnen. Beispielsweise können wir statt der vier einzelnen Zuweisungen nur eine einzelne Zuweisung nutzen, die gleich alle Komponenten auf einmal den entsprechenden Bezeichnern zuweist: scala> val (first, second, third, fourth) = myTuple first: java.lang.String = Hello second: Int = 1 third: Boolean = true fourth: Double = 5.7 Scala hat noch eine weitere Möglichkeit, mehreren Bezeichnern gleichzeitig einen Wert zuzuweisen. Es ist möglich, mehrere Bezeichner links vom Zuweisungsoperator mit Komma getrennt zu schreiben und auf der rechten Seite einen Ausdruck anzugeben, z.B. scala> val i, j = 12 i: Int = 12 j: Int = 12 Nutzen wir einen Wert eines veränderlichen Datentyps auf der rechten Seite, bekommt jede Variablen ihren eigenen Wert. In der folgenden Sitzung weisen wir zunächst m und n eine veränderliche, leere Menge zu. Anschließend fügen wir eine 12 in die Menge m ein und geben beide Mengen aus. Wie in der Ausgabe zu sehen 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 += 12 res0: m.type = Set(12) scala> println("m = "+m+"\nn = "+n) m = Set(12) n = Set()
3.1 Ein kleines bisschen Syntax
35
In diesem Fall kann m += 12 übrigens gar keine Abkürzung für m = m + 12 sein, denn m ist ja ein val. Der Versuch, die vermeintlich ausführliche Schreibweise zu nutzen, führt zu einer Fehlermeldung: scala> m = m + 12 :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 Funktionsdefinition immer mit dem Schlüsselwort def. Ihm folgen der Funktionsname und die Parameterliste9 . 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 Parametertypen nicht berechnen. Daher müssen diese in der Parameterliste immer angegeben werden. Der Funktionsaufruf besteht erwartungsgemäß aus dem Funktionsnamen und der Argumentliste: hello("Oliver") Wird wie in der hello-Funktion kein Wert zurückgegeben, so hat die Funktion den Ergebnistyp Unit10 . Soll der Rückgabetyp in der Funktionsdefinition dennoch explizit angegeben werden, muss vor der öffnenden, geschweiften Klammer ein Gleichheitszeichen stehen, d.h. die Funktion hello kann auch definiert werden durch: def hello(name: String): Unit = { println("Hello "+name) } Eine Funktion mit dem Ergebnistyp Unit wird in Scala Prozedur genannt. Üblicherweise werden der Typ Unit und das Gleichheitszeichen dann weggelassen. Soll eine Funktion ein Ergebnis berechnen, so muss immer ein Gleichheitszeichen verwendet 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.
36
3 Grundlagen
Wird der zuletzt berechnete Ausdruck als Ergebnis zurückgegeben, ist das Schlüsselwort 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 die geschweiften 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ällen nicht 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 werden: 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 es möglich, beim Aufruf einer Funktion die Argumente explizit zu benennen und in einer anderen Reihenfolge anzugeben. Mit Letzteren können die Argumente, für die ein Default-Wert definiert wurde, weggelassen werden. Die folgenden Funktionsaufrufe 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 vertauscht 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 Spezialfall erlaubt ist, in dem trotzdem die Reihenfolge eingehalten wird. Obwohl er
3.1 Ein kleines bisschen Syntax
37
zum richtigen Ergebnis führt, sollten Sie beim Mischen von benannten Argumenten und Positionsargumenten die Regel einhalten, dass alle nicht benannten Argumente vor allen benannten Argumenten stehen müssen, denn die Parameterliste wird zuerst der Reihe nach von vorne befüllt, beginnend mit den unbenannten Argumenten. Die beiden folgenden Ausdrücke sind daher nicht zulässig: sum(2,x=1) // nicht zulässig sum(y=2,1) // nicht zulässig Es ist auch möglich, eine variable Anzahl von Parametern zuzulassen. Angezeigt wird dies mit dem * nach Angabe des gemeinsamen Namens für alle Parameter und dem Typ eines einzelnen Parameters. Beispielsweise kann die folgende Funktion mit beliebig vielen Int-Argumenten aufgerufen werden. Jedes Argument wird dann auf einer eigenen Zeile ausgegeben: def printInts(ints: Int*) { for (int say.hello() Hi scala> sayAgain.hello() Hello Zunächst wird eine unveränderliche Variable say definiert, ein neues HelloObjekt erzeugt und dieses der Variable say zugewiesen. Damit wird für die Variable say der Typ Hello inferiert. Anschließend wird die Nachricht hello an das Objekt mit dem Namen say geschickt, das mit der Ausgabe der Zeichenkette Hello reagiert. Mit der dritten Eingabe werden ein zweites Objekt und eine zweite Variable erzeugt. Wie zu erwarten ist, bleibt der Begrüssungstext des sayAgain-Objekts Hello, obwohl der des say-Objektes mit der darauf folgenden Eingabe auf Hi geändert wurde, da jedes Objekt eigene Felder, sogenannte Instanzvariablen, hat. An dieser Stelle wird auch noch einmal deutlich, dass eine unveränderliche Variable durchaus ein veränderliches Objekt referenzieren kann. Das Feld greeting von say kann verändert werden. say kann aber kein anderes Objekt referenzieren. Der Versuch say = sayAgain wird mit error: reassignment to val quittiert.
50
4 Reine Objektorientierung
Wird in der Scala-Shell ein Objekt mit new erzeugt, wird es mithilfe der toString-Methode ausgegeben. Nachdem die Klasse Hello keine eigene toString-Methode besitzt, wird die Standardimplementierung genutzt. Da wir uns auf der Java-Plattform befinden, handelt es sich um die Implementierung aus java.lang.Object, die den Klassennamen, ein @-Zeichen und die hexadezimale Repräsentation des Hash-Codes des Objekts ausgibt. Um eine eigene toString-Methode zu implementieren, muss sie redefiniert werden. Die Klasse Hello kann beispielsweise um die folgende Methodendefinition ergänzt werden: override def toString = "Ready to say "+greeting Beim Redefinieren von Methoden ist der Modifier override4 zwingend vorgeschrieben. Wird er weggelassen, wird dies vom Scala-Compiler mit folgender Meldung quittiert: :13: error: overriding method toString in class Object of type ()java.lang.String; method toString needs ‘override’ modifier def toString = "Ready to say "+greeting Die Methode toString kann nun explizit aufgerufen werden bzw. sie wird beim Ausgeben des Objektes mit println automatisch ausgeführt. Nur in der ScalaShell wird sie beim Aufruf des Konstruktors automatisch genutzt. Die folgende Sitzung stellt dies beispielhaft dar: scala> val say = new Hello say: 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 durch einfache Zuweisung beliebig verändert werden. Um keine Änderung zuzulassen, ersetzen wir in der Definition einfach nur das var durch ein val. In vielen Fällen ist aber ein Mittelweg zwischen keiner und beliebiger Änderung das Gewünschte. In vielen objektorientierten Programmiersprachen wird dazu das Feld versteckt und Zugriff darauf nur über sogenannte Getter- und Settermethoden zugelassen. Scala geht einen für den Programmierer einfacheren Weg: Jedes Feld, das mit dem Schlüsselwort var definiert wurde, wird direkt als Paar von Getter- und Settermethode interpretiert. Das heißt, das Feld wird automatisch versteckt und eine Gettermethode mit demselben Namen sowie eine Settermethode mit dem Namen 4
Java hat eine @Override-Annotation, die nicht zwingend vorgeschrieben ist.
4.1 Klassen und Objekte
51
ergänzt um die Zeichen _=. Lesender Zugriff auf das Feld führt dann die Gettermethode aus, schreibender Zugriff die Settermethode. Um dennoch die übliche Syntax . = beizubehalten, wird daraus automatisch ._=() gemacht. Wird beispielsweise auf das Feld greeting lesend zugegriffen, so wird die Gettermethode greeting ausgeführt. Wird dem Feld greeting z.B. mit say.greeting="Hi" ein Wert zugewiesen, so wird die Settermethode greeting_= mit der rechten Seite 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 und wird mit dem Modifier private[this]5 versteckt. Zugriff auf das Feld erfolgt durch die Methoden greeting und greeting_=. Die bisher gezeigten Beispiele in der Scala-Shell laufen unverändert weiter. Listing 4.3: Die Klasse Hello
class Hello { private[this] var greet = "Hello" def greeting = greet def greeting_=(greeting: String) { if (greet == greeting) println("greeting is already set to \""+greet+"\"") else { greet=greeting println(this) } } // ... the rest goes here } Wird nun auf greeting lesend zugegriffen, wird der aktuelle Wert des Feldes greet zurückgegeben. Beim schreibenden Zugriff wird überprüft, ob der aktuelle Wert von greet bereits mit dem Parameter greeting übereinstimmt. Ist dies der Fall, wird dies ausgegeben. Andernfalls wird der übergebene Wert dem Feld greet zugewiesen und das Objekt mit println ausgegeben. Um dies zu verdeutlichen, ist im Folgenden wieder eine Beispielsitzung mit der Scala-Shell angegeben: scala> val say = new Hello say: Hello = Ready to say Hello 5
Die sogenannten Sichtbarkeitsmodifier werden auf Seite 56 erklärt.
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 das versteckte Feld nicht auch greeting heißen. Lokale Variablen und Parameter können aber, wie in Listing 4.3 zu sehen, denselben Namen wie Member benutzen. Um in solch einem Fall dennoch auf das verschattete Member zugreifen zu können, muss das Objekt explizit mit this referenziert werden. Beispielsweise kö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 Gettermethode zum Zugriff auf das Feld greet genutzt werden. Ein Paar von Getter- und Settermethode wird auch als Property bezeichnet – in Anlehnung an die Properties von C#, die jedoch eine spezielle Syntax haben. Getterund Settermethoden können auch ohne assoziierte Variable genutzt werden. Listing 4.4 zeigt ein Beispiel6 . Definiert werden ein Feld euro und eine Property dollar. Listing 4.4: Die Klasse Currency
class Currency { var exchangeRate = 2.0f var euro = 0.0f def dollar = euro * exchangeRate def 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 Rundungsfehler auftreten können. Der angegebene Kurs (exchangeRate) entspricht zugegebenermaßen auch nicht dem aktuellen Stand, macht die folgenden Beispiele aber übersichtlicher.
4.1 Klassen und Objekte
53
setzen lassen. Dazu ist es in Scala allerdings notwendig, einen Unterstrich zuzuweisen. Nachdem dann aber kein Typ mehr lokal inferiert werden kann, muss dieser angegeben werden. Das heißt, wir könnten die zweite Zeile in der Klasse Currency durch folgende Zeile ersetzen: var euro: Float = _ Ohne die Zuweisung des Unterstrichs entspräche euro einer abstrakten Variablen und Currency dadurch einer abstrakten Klasse (siehe Abschnitt 4.1.6). Die Standardwerte für die verschiedenen Datentypen sind 0 für die numerischen Datentypen, false für den Datentyp Boolean, () für den Typ Unit und null für alle Referenztypen. Für den Nutzer der Klasse Currency sieht es nun so aus, als hätte die Klasse zwei Felder: euro und dollar. In Wirklichkeit wird nur ein Wert im Feld euro gespeichert und beim Zugriff über dollar jedes Mal umgerechnet. Eine Beispielsitzung sieht folgendermaßen aus: scala> val m1, m2 = new Currency m1: Currency = 0.0 EUR / 0.0 USD m2: Currency = 0.0 EUR / 0.0 USD scala> m1.euro = 20 scala> m1 res0: Currency = 20.0 EUR / 40.0 USD scala> m2.dollar = 20 scala> m2 res1: Currency = 10.0 EUR / 20.0 USD Als Erstes werden zwei Values m1 und m2 definiert und jedem der beiden Werte ein eigenes Currency-Objekt zugewiesen. Scala lässt auch an dieser Stelle eine abkürzende Schreibweise zu. Anschließend wird dem Objekt m1 der Eurowert 20 zugewiesen, und beide Werte werden ausgegeben. Umgekehrt wird bei m2 der Dollarwert gesetzt. Die Klasse Currency enthält ein weiteres Feld, in dem der aktuelle Kurs gehalten wird. In Listing 4.4 haben wir das Feld als var definiert, also kann es von außen beliebig geändert werden. Eine Änderung des Kurses bedeutet in diesem Fall eine Änderung des Dollar-, aber keine Änderung des Eurowertes. Hätten wir umgekehrt 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-Objekte denselben Wert haben. Wenn er sich ändert, muss er in allen existierenden Objekten geändert werden, und neue Objekte müssen direkt mit dem neuen Kurswert erzeugt werden. Die Lösung wäre, den Kurs nicht in jedem Objekt zu speichern, sondern einmal an zentraler Stelle zu halten. Eine solche zentrale Stelle für Objekte einer Klas-
54
4 Reine Objektorientierung
se ist das sogenannte Companion-Objekt. Das Companion-Objekt hat den gleichen Namen wie die Klasse und muss zusammen mit der Klasse in einer QuellcodeDatei stehen. Dieses Singleton-Objekt ersetzt die statischen Felder und Methoden von Java und anderen objektorientierten Programmiersprachen zugunsten eines einheitlichen Objektmodells. Eine überarbeitete Version der Currency-Klasse mit einem Companion-Objekt ist in 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.0f def dollar = euro * Currency.exchangeRate def dollar_= (m: Float) { euro = m / Currency.exchangeRate } override def toString = euro+" EUR / "+dollar+" USD" } Auf den Kurs im Companion-Objekt kann nun mit Currency.exchangeRate zugegriffen werden. Wird der Kurs im Companion-Objekt geändert, hat dies wie gewünscht Auswirkungen auf alle existierenden und alle neuen Objekte, wie die folgende Sitzung zeigt: scala> val m1 = new Currency m1: Currency = 0.0 EUR / 0.0 USD scala> m1.euro = 20 scala> m1 res0: Currency = 20.0 EUR / 40.0 USD scala> Currency.exchangeRate = 4 scala> m1 res1: Currency = 20.0 EUR / 80.0 USD scala> val m2 = new Currency m2: Currency = 0.0 EUR / 0.0 USD scala> m2.dollar = 20 scala> m2 res2: Currency = 5.0 EUR / 20.0 USD
4.1 Klassen und Objekte
55
Nachdem der Kurs auf 4.0 geändert wurde, ist der Dollarwert von m1 von 40 auf 80 Dollar gestiegen. Für das nach der Kursänderung erzeugte Objekt m2 gilt sofort der neue Kurs. Wir kehren nun noch einmal zurück zu den Methoden und betrachten erneut die Klasse Hello. Der Übersichtlichkeit halber setzen wir noch einmal mit der ersten Version der Klasse aus Listing 4.2 auf Seite 48, mit einem Feld greeting und einer Methode hello, die den Begrüßungstext mithilfe von println ausgibt, an. Wir wollen die Klasse nun so erweitern, dass wir mit Namen begrüßt werden können. Ein erster Ansatz wäre, die Klasse durch eine Methode helloName zu erweitern, 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 folgende Sitzung zeigt: scala> val say = new Hello say: 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 Mal helloName nutzen müssten, sondern in beiden Fällen einfach denselben Namen hello verwenden könnten. Das ist auch möglich und nennt sich Überladen von Methoden. Beim Überladen von Methoden ist darauf zu achten, dass sich die Parameterlisten aller Methoden mit demselben Namen bezüglich Anzahl oder Typ unterscheiden. Die Methoden können auch verschiedene Ergebnistypen haben. Dies alleine reicht aber nicht zur Unterscheidung aus und wird vom Compiler nicht 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) } }
56
4 Reine Objektorientierung
Listing 4.6 zeigt die Klasse Hello, die zwei Methoden mit dem Namen hello besitzt. Die erste Methode hat keinen, die zweite Methode einen Parameter vom Typ String. Damit kann beim Aufruf der Methode über die Argumentliste entschieden werden, welche der beiden ausgeführt wird. Ist die Argumentliste leer, wird die erste Methode ausgeführt. Enthält die Argumentliste genau einen String bzw. einen Ausdruck, der zu einem String ausgewertet werden kann, wird die zweite 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 angegeben. Dadurch sind die Member alle public, das heißt aus jeder anderen Klasse sichtbar und nutzbar. Ein wesentliches Merkmal der objektorientierten Programmierung ist das information 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 Implementierung ändern zu können, ohne Gefahr zu laufen, dass Applikationen, die die Klasse nutzen, nicht mehr kompilierbar sind. Ziel ist es, das Application Programming 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 Modifier private und protected. Diese werden vor den entsprechenden Schlüsselwörtern 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 Zugriffsschutz auch für innere Klassen. Dabei gilt, dass aus einer weiter innen liegenden Klasse auf die mit private geschützten weiter außen liegenden Member zugegriffen werden kann. Umgekehrt gilt das allerdings nicht und weicht damit zum Beispiel von Java ab, wo in einer Klasse grundsätzlich alle Member einer inneren Klasse sichtbar sind. Für mit protected geschützte Member gilt, dass sie über eine Klasse hinaus nur in abgeleiteten Klassen sichtbar sind und nicht in allen Klassen des Packages wie in Java. Abgeleitete Klassen werden in Abschnitt 4.1.5 besprochen. Es ist darüber hinaus möglich, die Sichtbarkeit auf eine noch feingranularere Weise zu definieren. Dazu gibt es sogenannte Qualifier, die in eckigen Klammern nach private oder protected stehen, z.B. private[X] oder protected[Y]. X bzw. Y stehen dabei für Packages, Klassen oder Singleton-Objekte. Mit dem Modifier private[this] ist es möglich, den Zugriff auf ein Member so einzuschränken, dass nur innerhalb des Objekts selbst und nicht aus anderen Objekten der selben Klasse darauf zugegriffen werden kann, wie bei private.
4.1 Klassen und Objekte
57
Das in Listing 4.5 auf Seite 54 gezeigte Companion-Objekt hätte in dieser Konstellation auch ein beliebiges anderes Singleton-Objekt sein können. Dann müsste aus der Currency-Klasse auf den Wechselkurs eben nicht mit dem Ausdruck Currency.exchangeRate, sondern über den Namen des anderen Objektes zugegriffen werden. Interessant werden Companion-Objekte, wenn es um Zugriffsschutz geht. Es gilt nämlich für eine Klasse und ihr Companion-Objekt Folgendes: Aus der Klasse kann auf alle mit dem private-Modifier versehenen Member des CompanionObjektes zugegriffen werden und umgekehrt. Listing 4.7: SharedCounter.scala
class SharedCounter { private[this] var counted = 0 def 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 = 0 private def value = count private def increment() { count += 1 } } Listing 4.7 zeigt zur Veranschaulichung eine Klasse SharedCounter mit dem dazugehörigen Companion-Objekt7 . Die Zählvariable count befindet sich im Companion-Objekt und ist aufgrund von private[this] auch nur dort sichtbar. Die beiden Methoden value und increment sind außerdem in der Klasse SharedCounter sichtbar. Eine SharedCounter-Instanz kann den gemeinsamen Zähler mit SharedCounter.increment() hochzählen und auf den aktuellen Stand mittels SharedCounter.value zugreifen. Darüber hinaus hat jede SharedCounter-Instanz ein objekt-privates Feld counted, in dem festgehalten wird, wie oft diese Instanz den gemeinsamen Zähler inkrementiert hat. Auf dieses Feld kann nur innerhalb der entsprechenden Instanz zugegriffen werden. Aufgrund von Limitierungen der Scala-Shell lässt sich der Code aus Listing 4.7 weder in die Shell eingeben noch als Datei laden. Es muss kompiliert und kann beispielweise mit einer einfachen, wie in Listing 4.8 dargestellten, Applikation getestet werden. 7 Zur Erinnerung: Die Klasse SharedCounter und das Objekt SharedCounter müssen sich in einer Quellcode-Datei befinden. Die Verwendung des gleichen Namens alleine reicht nicht aus, um ein Companion-Objekt zu definieren.
58
4 Reine Objektorientierung Listing 4.8: Die Applikation CounterTest zum Testen des SharedCounters
object CounterTest extends Application { val sc1 = new SharedCounter val sc2 = new SharedCounter for (i 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 val array = new Array[String](2) array: Array[String] = Array(null, null) scala> array(0) = "World" scala> array(1) = "Reader"
60
4 Reine Objektorientierung
scala> for (i val say = new Hello say: Hello = Hello@1497b7b1 scala> say.greeting res0: say.Greeting = Hello 10
Ein Objektname ist der Name eines Singleton-Objekts oder der Bezeichner einer var oder val, die ein Objekt referenziert.
4.1 Klassen und Objekte
61
Wollen wir zu einer Klasse oder einem Objekt eine innere Klasse oder ein Objekt definieren, 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 folgendermaßen: scala> val x = new A x: A = A@1c7b0f4d scala> x.B.c res1: Int = 5 scala> x.f.c res2: Int = 9 scala> x.f.E.c res3: 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 zusammenfassen lassen, aber außerhalb nicht benötigt werden. Ein Beispiel ist in Listing 4.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 = _ } }
62
4 Reine Objektorientierung
Die Klasse Customer enthält ein Objekt Address, in dem die Adresse des Kunden gekapselt ist. Im folgenden Abschnitt lernen wir nun endlich die Konstruktoren kennen, mit denen wir die Felder beim Erzeugen des Objektes gleich sinnvoller belegen können.
4.1.3
Konstruktoren
Neue Objekte werden mit dem Schlüsselwort new konstruiert, das vor den Klassennamen geschrieben wird, z.B. new Hello. Es kann natürlich auch wie beispielsweise in Java üblich new Hello() geschrieben werden. Beides ist in Scala äquivalent und hat in diesem Fall natürlich nichts mit dem Uniform Access Principle zu tun. Üblicherweise werden in Scala zugunsten von knapperem und besser lesbarem Code die Klammern weggelassen. Wird ein Objekt mit new erzeugt, so wird dazu ein Konstruktor ausgeführt. Bisher haben wir keinen Konstruktor angegeben, sondern uns auf den automatisch generierten Konstruktor gestützt. Im Fall der Hello-Klasse wollen wir nun die Möglichkeit schaffen direkt beim Erzeugen eines Hello-Objektes einen eigenen Begrüßungstext zu übergeben. In Scala funktioniert das mit sogenannten Klassen-Parametern, die direkt nach dem Klassennamen 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 genauso wie das zuvor mit der Klasse aus Listing 4.2 und new Hello erzeugte Objekt. Der wesentliche Unterschied ist, dass auf greeting nicht außerhalb der Klasse zugegriffen werden kann, da mit dem Klassen-Parameter nicht automatisch ein Feld greeting definiert wurde. Scala erzeugt aus den Klassen-Parametern automatisch einen Konstruktor mit genau diesen Parametern, der als primary constructor bezeichnet wird. Ohne Parameter, also mit new Hello, kann jetzt kein Objekt mehr konstruiert werden. Soll zu einem Klassen-Parameter automatisch ein Feld mit demselben Namen erzeugt werden, muss der Parameter als val oder var deklariert werden. Es können zusätzlich noch Sichtbarkeitsmodifier vorangestellt werden.
4.1 Klassen und Objekte
63
Listing 4.13: Die Klasse Hello mit 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 einem Hello-Objekt nach dem Erzeugen der Begrüßungstext wie zuvor geändert werden, 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 unmittelbar 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 Erzeugen eines Objekts überprüft, dass der übergebene String nicht leer ist. Das Ausrufungszeichen steht dabei für die logische Negation, also „der übergebene String 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 Konstruktor zur Verfügung zu stellen, um Objekte mit unterschiedlichen Parameterlisten erzeugen zu können. Die weiteren Konstruktoren werden als auxiliary constructors bezeichnet und mittels def this(...) definiert. Jeder zusätzliche Konstruktor muss als Erstes einen anderen Konstruktor mit this(...) aufrufen. Ziel ist es, damit sicherzustellen, dass beim Erzeugen eines Objektes die Anweisungen des primären Konstruktors vor den Anweisungen der zusätzlichen Konstrukto11
require ist eine ganz normale Methode, die im Objekt Predef definiert ist und an damit an beliebiger Stelle genutzt werden kann. Sie wirft eine IllegalArgumentException, wenn das Argument zu false ausgewertet wird.
64
4 Reine Objektorientierung
ren ausgeführt werden und es somit einen einzigen Einstiegspunkt in die Klasse gibt. 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 erzeugtes Objekt antwortet auf hello dann mit Hello. Das leere Klammerpaar darf bei der Definition des Konstruktors nicht weggelassen werden. Wird ein zusätzlicher Konstruktor aber nur benötigt, um einen Standardwert zu setzen, kann dies seit Scala 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 anschließend Objekte mit new Hello("Hi"), aber auch mit new Hello erzeugt werden, ohne dass es einen zusätzlichen Konstruktor gibt. Der primäre Konstruktor kann aufgrund des Default-Arguments auch mit einer leeren Argumentliste aufgerufen werden. Scala bietet darüber hinaus noch die Möglichkeit, Objekte ohne new zu erzeugen. Zum Beispiel erzeugt List(1,2,3) eine Liste mit den Elementen 1, 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 expandiert zu List.apply(1,2,3). Das heißt, apply ist eine Methode des CompanionObjektes. Wird die Methode dazu genutzt, ein Objekt der dazugehörigen Klasse zu 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)
4.1 Klassen und Objekte
65
def hello() { println(greeting) } } object Hello { def apply() = new Hello def apply(greeting: String) = new Hello(greeting) } Wird die Klasse Hello mit den beiden apply-Methoden im Companion-Objekt wie in Listing 4.17 dargestellt definiert, können anschließend Hello-Objekte sowohl mit Hello() als auch mit Hello() erzeugt werden. Würde das leere Klammerpaar im ersten Fall weggelassen, so würde der Ausdruck das Companion-Objekt selbst bezeichnen, d.h. nach Auswertung der Zeile val say = Hello ist say kein Hello-Objekt, sondern eine Referenz auf das Singleton-Objekt Hello. Daher versteht say die Nachricht hello nicht. Stattdessen kann dann aber 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 einfache Programme ausführbar zu machen. Wie in Abschnitt 3.3 auf Seite 43 dargestellt, wird der gesamte Code, der direkt im definierten Objekt und nicht innerhalb eines Members steht, beim Starten ausgeführt. Dies ist ein grundsätzliches Feature von Scala, da dieser Code ja in den primären Konstruktor aufgenommen und bei Initialisierung eines Objektes ausgeführt wird. Das Starten eines Objektes initialisiert es als Erstes. Anschließend wird die main-Funktion des Application-Traits ausgeführt und das Programm beendet.
4.1.4
Enumerations
Bisher haben wir den Begrüßungstext als String repräsentiert. Damit haben wir eine sehr flexible Implementierung gewählt, die aber beliebige Werte zulässt. So kann beispielsweise ein Nutzer der Hello-Klasse mit new Hello ("Tankstelle") ein Hello-Objekt erzeugen, das auf die Nachricht hello mit Tankstelle antwortet. Eine einfache Möglichkeit wäre es, den Klassen-Parameter mit einer Liste zulässiger Werte zu vergleichen, wie in Listing 4.18 dargestellt. Im Companion-Objekt wird eine Liste zulässiger Werte für greeting definiert und im primären Konstruktor 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) }
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 Wert erzeugt, z.B. new Hello("Salve"), kann der Quelltext ohne Fehlermeldung übersetzt werden12 . Erst beim Ausführen des Programms meldet sich die Laufzeitumgebung mit java.lang.ExceptionInInitializerError at 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 zu können. Viele Programmiersprachen bieten dafür sogenannte Aufzählungstypen oder auf Englisch Enumerations, mit denen eine endliche Anzahl von Werten festgelegt werden kann. Auch in Scala können Aufzählungstypen definiert werden, allerdings ganz nach der Scala-Philosophie auch wieder nicht in spezieller Sprachsyntax, 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 definiert. Dazu wird die Klasse Enumeration erweitert. Die Werte haben den Typ Greeting.Value. Jeder Aufruf der Methode Value erzeugt einen neuen Wert, der Teil der Enumeration wird. Überladene Versionen der Methode Value ermöglichen es, eine Integer- bzw. eine spezielle String-Repräsentation des Wertes anzugeben, 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 Klasse kann dieser Code nicht in der Scala-Shell eingegeben werden.
4.1 Klassen und Objekte
67
Soll die Klasse Hello nun den Aufzählungstyp nutzen, bekommt der KlassenParameter greeting den entsprechenden Typ Greeting.Value und der Default-Wert wird auf Greeting.Hello gesetzt, siehe Listing 4.20. Durch Definition eines Typsynonyms, den Default-Wert und ein Import-Statement13 vor der Hello-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 und Default-Wert
object Greeting extends Enumeration { type Greeting = Value val 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 werden. Wird nun beispielsweise versucht, Programmcode zu übersetzen, der new Hello(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 die Ableitung einer Klasse von einer anderen. Die abgeleitete Klasse wird als Subklasse, die Klasse, von der abgeleitet wurde, als Basisklasse oder auch Superklasse bezeichnet. Beim Ableiten erbt die Subklasse alle nicht-privaten Member der Basisklasse, d.h. die Member sind Bestandteil der Subklasse, als wären sie erst darin definiert worden. Bisher haben wir bei unseren Klassendefinitionen keine Basisklasse angegeben. Dadurch ist die Basisklasse automatisch die Klasse scala.AnyRef, die auf der Java-Plattform der Klasse java.lang.Object entspricht. Die in AnyRef definierten Methoden sind daher auch in allen Klassen verfügbar, z.B. können zwei Hello-Objekte mit der Methode == verglichen werden, ohne dass diese Methode explizit 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 Member 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.
68
4 Reine Objektorientierung
Soll von einer anderen Klasse geerbt werden, muss dies mit dem Schlüsselwort extends 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 hello aus der Klasse Hello. Daher versteht ein HelloAndGoodbye-Objekt sowohl die Nachricht goodbye als auch hello. Die folgende Sitzung veranschaulicht dies. scala> val say = new HelloAndGoodbye say: HelloAndGoodbye = HelloAndGoodbye@4e836869 scala> say.hello() Hello scala> say.goodbye() Goodbye Beim Verwenden der Methoden sehen wir keinen Unterschied, ob die Methode in der Klasse definiert oder von einer Basisklasse geerbt wurde. Nachdem jede Klasse auch einen Datentypen definiert, stellt sich die Frage, ob die Datentypen der Sub- und Basisklasse in irgendeiner Weise zusammenhängen. Tatsächlich ist es so, dass der Typ der Subklasse ein sogenannter Subtyp des Typs der Basisklasse ist. Die Subklasse ist eine Spezialisierung der Basisklasse, und umgekehrt 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 nutzen und 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 den zwei Klassen-Parametern greeting und goodbye inklusive Default-Argumenten. Der Klassen-Parameter greeting wird als Parameter an die Basisklasse Hello übergeben. Dies entspricht dem Aufruf des Konstruktors der Basisklas-
4.1 Klassen und Objekte
69
se, der in Java beispielsweise mit dem Schlüsselwort super erfolgt. In Scala kann der Aufruf des Konstruktors der Basisklasse ausschließlich auf die in Listing 4.23 dargestellte Weise erfolgen. Das Schlüsselwort super gibt es in Scala aber dennoch, und zwar zum Aufruf einer Methode der Basisklasse. Dies ist beispielsweise nützlich, wenn die geerbte Methode redefiniert wird und an einer Stelle doch die Methode der Basisklasse verwendet werden soll oder in Stackable Traits, die in Abschnitt 4.3.2 erklärt werden. Wird ein HelloAndGoodbye-Objekt ohne Argumente erzeugt, so wird der Begrüßungstext auf den in HelloAndGoodbye angegebenen Default-Wert Hi gesetzt. Der Default-Wert von greeting aus der Klasse Hello kommt, so wie die beiden Klassen implementiert sind, nicht mehr zum Tragen, da immer der Default-Wert von greeting aus der Definition von HelloAndGoodbye genutzt wird. Wird dieser Default-Wert einfach weggelassen, ist es nicht mehr möglich, ein HelloAndGoodbye-Objekt ohne Argumente zu erzeugen, da der Aufruf des Konstruktors in der Klasse HelloAndGoodbye nicht ohne das greetingArgument aufgerufen werden kann. Listing 4.24: Klassen-Parameter in Hello und HelloAndGoodbye nutzen den selben Default-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 wie in Listing 4.24 realisiert werden. Der Default-Wert wird dort im Feld defaultGreeting des Objektes Hello gespeichert und kann so in den beiden Klassen Hello und HelloAndGoodbye genutzt werden. Listing 4.25: Fehlerhaftes Redefinieren eines Feldes
class Hello(var greeting: String) { def hello() {
70
4 Reine Objektorientierung
println(greeting) } } class HelloAndGoodbye( var greeting: String, // does not compile var farewell: String ) extends Hello(greeting) { def goodbye() { println(farewell) } } Wenn Sie Felder aus den Klassen-Parametern erzeugen lassen, müssen Sie darauf achten, dass die Felder, solange sie nicht private sind, auch vererbt werden. Im Quelltext in Listing 4.25 wird das erzeugte Feld greeting von Hello durch das Feld greeting in HelloAndGoodbye redefiniert. Nachdem dort aber ein override stehen muss, schlägt das Kompilieren mit folgender Meldung fehl: HelloAndGoodbye.scala:7: error: overriding variable greeting in class Hello of type String; variable greeting needs ‘override’ modifier var greeting: String, // does not compile ^ one error found Das Redefinieren des Feldes ist in Listing 4.25 ohnehin überflüssig, da das Feld greeting von Hello geerbt und damit sowieso schon verfügbar ist. Ein zugegebenermaßen etwas konstruierter Anwendungsfall läge beispielsweise in Listing 4.26 vor. Dort wird zunächst eine Klasse HelloDefault definiert mit einem unveränderlichen Feld greeting, das nicht durch einen Klassen-Parameter gesetzt werden kann. Um in der Subklasse Hello diesen Wert mit einem KlassenParameter setzen zu können, muss es dort redefiniert werden. Wird dies nicht gemacht, wird auch ein mit new Hello("Hi") erzeugtes Objekt auf die Nachricht hello 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 einer Klasse leer ist wie bei der Klasse Hello, können die geschweiften Klammern weggelassen werden.
4.1 Klassen und Objekte
71
Übrigens kann das Redefinieren auch verhindert werden, indem das Member in der Basisklasse mit dem Modifier final versehen wird. Listing 4.27 zeigt ein Beispiel 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 einer Fehlermeldung quittiert, wie in der folgenden Sitzung zu sehen ist: scala> class Hi extends AlwaysHello { | override val greeting = "Hi" | } :7: error: overriding value greeting in class AlwaysHello of type java.lang.String("Hello"); value greeting cannot override final member override val greeting = "Hi" Besteht zwischen zwei Klassen eine Vererbungsrelation, so kann ein Objekt der Subklasse einer Variablen vom Typ der Basisklasse zugewiesen werden. Es kann z.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 die Methode goodbye über die Variable sayG nicht ausgeführt werden: scala> sayG.goodbye() :9: error: value goodbye is not a member of Hello sayG.goodbye Der Aufruf der Methode hello funktioniert aber erwartungsgemäß: scala> sayG.hello() Hi Betrachten wir nun eine weitere von Hello abgeleitete Klasse HelloTwice, die in Listing 4.28 dargestellt ist. Listing 4.28: Von der Klasse Hello abgeleitete Klasse HelloTwice
class HelloTwice(greeting: String) extends Hello(greeting) {
72
4 Reine Objektorientierung
override def hello() { println(greeting+", "+greeting) } } In der Klasse HelloTwice wird die von Hello geerbte Methode hello redefiniert. Statt den Begrüßungstext einmal auszugeben, wird er nun zweimal mit einem Komma getrennt ausgegeben. Erzeugen wir nun ein HelloTwice-Objekt und weisen es einer Hello-Variablen zu, passiert beim Aufruf von hello Folgendes: 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 HelloTwice ausgeführt und Salut, Salut ausgegeben. Diese Phänomene lassen sich durch Polymorphie und dynamisches Binden erklären. Die Variable sayT ist vom Typ Hello. Als solches kann sie ein Objekt referenzieren, das den Typ Hello hat. Dies beinhaltet auch Subtypen, da es sich dabei um eine sogenannte Spezialisierung handelt, das heißt ein HelloTwice-Objekt ist ein spezielles Hello-Objekt. Die Variable sayT ist also polymorph, zu deutsch vielgestaltig. Das Gleiche gilt natürlich auch für die Variable sayG. Beim Aufruf von Methoden der Variablen sayG und sayT kommen für den Compiler nun aber nur Methoden in Betracht, die in der Klasse Hello definiert worden sind. Daher schlägt der Versuch say.goodbye() zu nutzen, bereits beim Kompilieren fehl. In der Klasse Hello gibt es keine Methode goodbye. Um zu verstehen, dass sayT.hello() nun aber die Methode aus HelloTwice nimmt, hilft es, den Methodenaufruf noch einmal formal als Senden von Nachrichten zu betrachten. Wird an die Variable sayT die Nachricht hello gesendet, überprüft der Compiler, ob ein Objekt, das durch sayT referenziert wird, die Nachricht überhaupt verstehen könnte. Nachdem sayT den Typ Hello hat und die Klasse Hello die Methode hello enthält, ist dies zulässig. Zur Laufzeit wird nun tatsächlich die Nachricht an das von sayT referenzierte Objekt gesendet. Das HelloTwice-Objekt reagiert darauf mit der Ausführung seiner hello-Methode. Das heißt also, die Zulässigkeit des Methodenaufrufs wird statisch geprüft, aber der tatsächliche Methodenaufruf wird dynamisch gebunden. In abgeleiteten Klassen können nicht nur Methoden, sondern grundsätzlich alle von der Basisklasse geerbten Member redefiniert werden. Dazu ist jedes Mal der override-Modifier der neuen Definition voranzustellen. Auf Seite 58 haben wir bereits das Uniform Access Principle erläutert, nachdem es für die Verwendung einer Klasse transparent sein muss, ob ein Attribut als Feld
4.1 Klassen und Objekte
73
oder Methode implementiert wird. Dieser Ansatz wird auch auf die Vererbung ausgeweitet. Enthält eine Klasse eine parameterlose Methode, kann diese durch ein val redefiniert werden. Interessant ist dies insbesondere im Zusammenhang mit 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 abstrakt ist. Ein Member heißt abstrakt, wenn es keine vollständige Definition hat. Enthält eine Klasse kein abstraktes Member, heißt sie konkret. Dieses in den meisten objektorientierten Programmiersprachen enthaltene Konzept legt das Vorhandensein eines Members in einer Klasse fest und fordert damit von allen konkreten Subklassen, dass es dort implementiert wird. Zur Verdeutlichung wählen wir ein in vielen Programmierbüchern verwendetes Beispiel. Wir wollen Hunde, Katzen und Kühe modellieren. Ein erster Ansatz wäre die direkte und unabhängige Definition 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, Katzen und Kühe erzeugt werden und mit makeNoise ein Geräusch machen: scala> val lassie = new Dog lassie: Dog = Dog@1b9f9088 scala> lassie.makeNoise() woof, woof scala> val mauzi = new Cat mauzi: Cat = Cat@25d3e3f3 scala> mauzi.makeNoise() miaow
74
4 Reine Objektorientierung
scala> val emma = new Cow emma: Cow = Cow@645b56c4 scala> emma.makeNoise() moo Obwohl jedes Tier eine makeNoise-Methode hat, ist es nicht möglich, verschiedene Tiere einfach in eine Liste zu speichern und mit einer for-Schleife an jedes die Nachricht makeNoise zu senden: scala> val animals = List(lassie, mauzi, emma) animals: List[ScalaObject] = List(Dog@1b9f9088, Cat@25d3e3f3, Cow@645b56c4) scala> for (animal for (animal 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. Mit dem angegebenen Statement werden Calendar und GregorianCalendar aus java.util importiert und können anschließend ohne das Präfix java.util genutzt werden. Importe werden ausführlich in Abschnitt 4.2 besprochen. Durch die Definition von trixi entsteht der Eindruck, die Klasse Animal könnte doch instanziiert werden, zumal die Scala-Shell auch noch den Typ Animal für trixi inferiert. Dies ist aber nicht der Fall. Durch new Animal(...){...} wird eine anonyme Klasse definiert, die Animal erweitert. Nachdem die Klasse keinen eigenen Namen hat, definiert sie auch keinen neuen Typ. Daher bekommt trixi den Typ Animal.
78
4 Reine Objektorientierung
4.2
Codeorganisation
Spätestens sobald wir mit mehreren Klassen und Vererbung arbeiten, ist es wichtig, den Code möglichst übersichtlich zu organisieren. In Scala lässt sich der Quellcode in Packages gruppieren. Nachdem wir bisher kein Package explizit angegeben haben, befand sich alles im unbenannten Package. Das Definieren benannter Packages wird in Abschnitt 4.2.1 erklärt. Eine Neuerung in Scala 2.8 stellen die in Abschnitt 4.2.2 besprochenen Package Objects dar, mit denen auch andere Elemente außer Klassen und Objekten zu Packages hinzugefügt werden können. Um nicht immer den voll qualifizierten Namen einer Klasse oder 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 gesamte Quellcode dieser Datei in dem angegebenen Package. Eine Package-Klausel beginnt mit dem Schlüsselwort package, gefolgt vom Package-Namen. Packages können hierarchisch angeordnet werden. Die Trennung der Ebenen erfolgt mit einem Punkt. Beispielsweise wird durch die Angabe der Klausel package org.obraun.scalabook der gesamte Quellcode der Datei dem Package scalabook im Package obraun im Package org zugeordnet. In Scala kann dies auch durch drei aufeinander folgende Package-Klauseln angegeben werden: package org package obraun package scalabook Wie diese Schreibweise schon erahnen lässt, können in Scala Packages auch andere Packages enthalten. Daher muss das, was sich in einem äußeren Package befindet, nicht mit dem voll qualifizierenden Package-Namen angesprochen werden muss. Befinden wir uns beispielsweise in einer Klasse innerhalb des Packages scalabook, so können wir die Klasse org.obraun.misc.Other mit misc.Other referenzieren. Seit Scala 2.8 gibt es aber in den beiden oben angegebenen Schreibweisen einen Unterschied: Wenn die Packages wie im ersten Fall direkt mit einem Punkt getrennt zusammengeschrieben werden, wird nicht in den Zwischen-Packages gesucht, sondern nur Top-Level. Werden die Packages einzeln mit jeweils einer Package-Klausel angegeben, wird auch dazwischen gesucht, was vor 2.8 in beiden 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-
4.2 Codeorganisation
79
sen sich die beiden Ansätze beliebig kombinieren. Beispielsweise wird mit den Klauseln package org package obraun.scalabook zuerst im Package org, dann Top-Level gesucht. Scala-Packages können aber nicht nur für eine gesamte Quellcode-Datei festgelegt werden, sondern auch für Teile davon. Dazu ist es möglich, das, was zu einem Package gehören soll, nach der Package-Klausel in geschweifte Klammern einzufassen. Das macht es möglich, verschiedene Teile einer Datei verschiedenen Packages zuzuordnen. In Listing 4.34 ist ein Beispiel dargestellt. Listing 4.34: Quellcode-Datei mit Code für mehrere Packages
package org { class Org package 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 wir die folgenden Verzeichnisse und Class-Dateien in der dargestellten Struktur vor: org +-| | | | +-+--
4.2.2
obraun +-- OrgObraun.class +-- scalabook +-- OrgObraunScalabook.class +-- OrgObraunScalabook$.class Org.class scala +-- OrgScala.class
Package Objects
Mit Scala 2.8 wurde unter anderem das Collection-Framework (siehe Abschnitt 6.2) neu organisiert. Dadurch wanderten Klassen von einem Package in ein anderes. Um eine möglichst sanfte Migration für bestehende Scala-Software zu gewährleisten, liegt die Idee nahe, in den Packages, aus denen die Klasse verschwunden ist, einfach ein Typsynonym auf die neue Klasse und einen Wert,
80
4 Reine Objektorientierung
der das neue Companion-Objekt referenziert, zu definieren. Beispielsweise ist die List aus dem Package scala in das Package scala.collection.immutable verschoben worden. Daher müsste in das Package scala nur Folgendes eingefügt werden, um die Liste nach wie vor als scala.List zur Verfügung zu stellen: 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 Konstrukte fast beliebig ineinander verschachtelt werden können, dürfen Packages nur 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üsselwort package dem Schlüsselwort object vorangestellt wird und es in einer Quellcode-Datei mit dem Namen package.scala in dem Verzeichnis des entsprechenden Packages abgespeichert wird. Alles, was in dem Package-Objekt definiert ist, wird Teil des entsprechenden Packages. Das heißt, im oben angegebenen Beispiel mit der Liste müssen die beiden Zeilen im Package-Objekt scala in der Datei package.scala im Verzeichnis scala enthalten 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 ihren einfachen Namen ohne qualifizierende Packages nutzbar zu machen. Beispielsweise 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 der Import beispielsweise innerhalb einer Funktion wie z.B. def fun = { import java.io.File // do something with File }
4.2 Codeorganisation
81
ist die einfache Bezeichnung File bis zur schließenden geschweiften Klammer möglich. Anschließend muss wieder java.io.File geschrieben werden, außer es gibt auch dort eine Import-Klausel. Es können durch Verwendung des Unterstrichs auch alle Member importiert werden. Beispielsweise werden mit import java.io._ alle Member des Packages java.io importiert. Es ist auch möglich, mit derselben Syntax 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 dem einfachen Bezeichner noOfM zugegriffen werden kann. Ohne den Import müssten wir Mountain.noOfM schreiben. Dies funktioniert selbstverständlich für beliebige 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 = 0 def numberOfMountains = noOfM } Statt nur ein Member oder gleich alle zu importieren, können einzelne Member in einer import selector clause zusammengefasst werden. Beispielsweise werden mit import java.util.{Calendar, GregorianCalendar} sowohl java.util.Calendar als auch java.util.GregorianCalendar importiert. Der Import von java.util.{_} entspricht im Übrigen dem Import von java.util._. Innerhalb einer Import-Selector-Klausel können einzelne Elemente auch umbenannt werden. Beispielsweise ist nach dem Import import java.util.{Calendar => Cal, GregorianCalendar} java.util.Calendar unter dem Bezeichner Cal und java.util.GregorianCalendar unter dem Namen GregorianCalendar verfügbar. Sollen einzelne Elemente umbenannt werden, aber dennoch alle importiert werden, können wir dies auch in einem Einzeiler angeben. Z.B. steht im folgenden Beispiel der Unterstrich 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 importiert werden können. 16 . . . aber aufgrund des private-Modifiers wieder nicht in der Scala-Shell.
82
4 Reine Objektorientierung
Es ist auch möglich, alles außer bestimmten Elementen zu importieren. Dazu wird auch die Syntax zur Umbenennung genutzt, allerdings dieses Mal mit dem Unterstrich als neuem Namen. Mit der folgenden Klausel wird alles aus java.io auß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ätere Importe frühere überschatten. Beispielsweise ist eine Klasse mit dem Namen StringBuilder sowohl in java.lang als auch in scala enthalten. Durch die obige Reihenfolge referenziert der Bezeichner StringBuilder dann scala.StringBuilder.
4.3
Traits
Scala unterstützt nur Einfachvererbung wie viele moderne objektorientierte Programmiersprachen. Das heißt, eine Klasse kann nur genau eine Basisklasse haben. Natürlich kann die Basisklasse auch schon eine andere Klasse erweitern. Mehrfachvererbung, wie sie beispielsweise in C++ möglich ist, kann zu einem undurchsichtigen und komplizierten Design führen und ermöglicht das Auftreten des sogenannten Diamond-Problems17 . Das Diamond-Problem entsteht, wenn eine Klasse D von zwei Basisklassen B und C abgeleitet ist und diese beiden wiederum von der Klasse A. Den Namen verdankt das Problem der grafischen Darstellung der Beziehung zwischen den vier Klassen, die wie eine Raute (engl. diamond) aussieht. Hat nun A eine Methode, die sowohl in B als auch in C redefiniert wird, ist nicht klar, welche Implementierung für D gelten sollte. Um nicht ganz auf eine einzige Basisklasse eingeschränkt zu sein, haben viele objektorientierte Programmiersprachen das Konzept der Interfaces eingeführt. Ein Interface kann als sehr spezielle abstrakte Klasse gesehen werden, die keine einzige konkrete Implementierung enthält. Das heißt aber, dass jede konkrete Klasse, die ein Interface implementiert, für alle Methoden des Interfaces eine Implementierung enthalten muss. Der Ansatz führt entweder zu sehr schlanken Interfaces, also Interfaces mit sehr wenigen Methoden, deren Implementierungen oft umständlich zu nutzen sind, oder zu Interfaces, bei deren Implementierung eine große Anzahl Methoden zu erstellen sind, die unter Umständen in vielen Klassen gleich aussehen. 17
siehe [TJJV04]
4.3 Traits
83
Scala hat keine Interfaces, sondern sogenannte Traits, die neben abstrakten Methoden auch Implementierungen von Methoden sowie Felder enthalten dürfen. In Abschnitt 4.3.1 werden Traits als Rich Interfaces besprochen. Außerdem ist es möglich, mithilfe von Traits eine Methode zu verändern. Dies wird in Abschnitt 4.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. Die abstrakte Klasse Animal aus Listing 4.32 auf Seite 76 könnte als Trait wie in Listing 4.37 dargestellt aussehen. Listing 4.37: Der Trait Animal
import java.util.Calendar trait Animal { var name: String val dateOfBirth: Calendar def makeNoise(): Unit def age = { val today = Calendar.getInstance val age = today.get(Calendar.YEAR) dateOfBirth.get(Calendar.YEAR) today.set(Calendar.YEAR, dateOfBirth.get(Calendar.YEAR)) if (today before dateOfBirth) age-1 else age } override def toString = { name+" "+age+" year"+(if (age!=1) "s" else "")+" old" } } Die Unterschiede zwischen dem Trait Animal (Listing 4.37) und der abstrakten Klasse 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.33 auf Seite 77 der Klassen-Parameter dateOfBirth als val definiert und nicht als Klassen-Parameter an Animal übergeben. Die Klasse Dog, die den Trait Animal verwendet, ist in Listing 4.38 angegeben.
84
4 Reine Objektorientierung Listing 4.38: Die Klasse Dog
import java.util.Calendar class 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, wird das Schlüsselwort extends weiterhin verwendet. Ein Trait ist eine spezielle Form des sogenannten Mixins. Ein Mixin fasst mehrfach verwendbare, zusammengehörige Funktionalitäten zu einer Einheit zusammen, die in eine Klasse hineingemixt werden kann. Der Name kommt vom englischen mix in (einmischen, unterrühren, hineinmixen). Die Besonderheit des Traits ist, dass eine Klasse, die einen Trait hineinmixt, aber keine Basisklasse mit extends angibt, die Basisklasse des Traits übernimmt. In unserem Beispiel hat der Trait Animal die Basisklasse AnyRef, die damit auch zur Basisklasse von Dog wird. Nachdem der vorgestellte Ansatz, Animal als Trait und nicht als Klasse zu implementieren, etwas konstruiert ist, wollen wir das Beispiel nun etwas anders aufbauen. 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: String def 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 werden kann. Stattdessen definieren wir einen Trait HasAge (siehe Listing 4.40). Listing 4.40: Der Trait HasAge
trait HasAge { import java.util.Calendar val dateOfBirth: Calendar def age = { val today = Calendar.getInstance val age = today.get(Calendar.YEAR) dateOfBirth.get(Calendar.YEAR) today.set(Calendar.YEAR, dateOfBirth.get(Calendar.YEAR)) if (today before dateOfBirth)
4.3 Traits
85
age-1 else age } } Wollen wir jetzt die Klasse Dog so implementieren, dass sie die gleiche Funktionalität wie zuvor hat, müssen wir die Klasse Animal erweitern und den Trait HasAge hineinmixen. Wird ein Trait zusätzlich zu einer Basisklasse oder zu einem anderen Trait hineingemixt, folgt dieser auf das Schlüsselwort with. Weitere Traits 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.Calendar class 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 die Angabe von with HasAge. Allerdings muss nach diesem Design die toStringMethode in Dog, und analog auch in Cat und Cow, redefiniert werden, da in der Klasse Animal die Methode age nicht bekannt ist und erst durch das Trait HasAge eingebracht wird. Der Ausweg ist aber ganz einfach. Nicht in die Klasse Dog, sondern schon in die Klasse Animal sollte der Trait HasAge hineingemixt werden. Die Klasse Animal mit dem Trait HasAge ist in Listing 4.42 aufgeführt. Die Klasse Dog kann dann wieder 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: String def makeNoise(): Unit override 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, zu definieren. Nachdem ein Trait aber keine Klassen- bzw. „Trait”-Variablen zulässt,
86
4 Reine Objektorientierung
müssen abstrakte Member des Traits entsprechend definiert werden. Betrachten wir den in Listing 4.43 definierten Trait PositiveNumber. Listing 4.43: Der Trait PositiveNumber
trait PositiveNumber { val value: Int require(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 nicht kleiner oder gleich Null ist. Wenn wir nun eine anonyme Klasse, die den Trait enthält, definieren wollen, wäre der naheliegende Ansatz, den val value wie in der folgenden Sitzung dargestellt zu definieren: scala> val n = new PositiveNumber { | val value = 12 | } java.lang.IllegalArgumentException: requirement failed at scala.Predef$.require(Predef.scala:134) ... Wie zu sehen ist, führt dies aber zu einer Exception. Dies liegt daran, dass beim Erzeugen eines Objektes zuerst der Standardwert für Int zugewiesen und mit require überprüft wird. Erst anschließend würde der Wert auf 12 gesetzt werden. 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.value res0: Int = 12 Wir wollen aber das require im Trait behalten und müssen daher nach einem anderen Ausweg suchen. Davon gibt es grundsätzlich sogar zwei: vorinitialisierte Felder und lazy vals. Lazy vals werden in Abschnitt 5.1 besprochen. Ein Feld wird vorinitialisiert, wenn es vor dem Trait in geschweiften Klammern definiert wird. Das heißt, wenn wir unsere positive Zahl n wie im folgenden Listing angegeben definieren, sieht das require den korrekten Wert 12:
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. Der Trait Ordered ist in Listing 4.44 dargestellt. Listing 4.44: Der Ordered-Trait
trait def def def def def def }
Ordered[A] extends java.lang.Comparable[A] { compare(that: A): Int < (that: A): Boolean = (this compare that) < > (that: A): Boolean = (this compare that) > = compareTo(that: A): Int = compare(that)
0 0 0 0
Objekte einer Klasse, die den Trait implementiert, können mit , = verglichen werden. Dazu muss nur die abstrakte Methode compare implementiert werden, die das Objekt (this) mit einem anderen Objekt (that) vergleicht und als Ergebnis ein x liefert, für das gilt: x0
genau dann, wenn genau dann, wenn genau dann, wenn
this < that this == that this > that
Wollen wir jetzt beispielsweise dem Trait HasAge aus Listing 4.40 das Feature hinzufügen, alle dem Alter nach vergleichen zu können, müssen wir den Trait wie in Listing 4.45 angegeben erweitern. Listing 4.45: Der Trait HasAge implementiert den Trait Ordered.
trait HasAge extends Ordered[HasAge] { import java.util.Calendar val dateOfBirth: Calendar def age = { val today = Calendar.getInstance val age = today.get(Calendar.YEAR) dateOfBirth.get(Calendar.YEAR) today.set(Calendar.YEAR, dateOfBirth.get(Calendar.YEAR)) if (today before dateOfBirth) age-1 else age } def compare(that: HasAge) = this.age compare that.age }
88
4 Reine Objektorientierung
Die einzigen beiden Stellen, die gegenüber dem ursprünglichen Trait aus Listing 4.40 anders sind, sind in der ersten Zeile das extends Ordered[HasAge] und die einzeilige Definition von compare, die sich einfach auf das in der Klasse Int definierte compare abstützt.
4.3.2
Stapelbare Modifikationen
Mit Traits ist es möglich, verschiedene Modifikationen hintereinander zu schachteln. Wir wollen dies mit folgendem einfachen Anwendungsfall zur Eingabe von Zeichenketten verdeutlichen: 1. Eine Zeichenkette soll als Ergebnis einer Funktion get zurückgegeben werden. 2. Eine Zeichenkette soll aus verschiedenen Quellen kommen können, z.B. von einer Benutzereingabe oder aus einer Datei. 3. Bei der Benutzereingabe soll es möglich sein, optional einen Prompt anzugeben. 4. Es soll die Möglichkeit geben, die Zeichenkette beim Einlesen, z.B. zur Fehlersuche im Programm, auf der Konsole auszugeben. 5. Es soll die Möglichkeit geben, die Zeichenketten zusätzlich in einer Liste zu speichern und damit zu einem beliebigen Zeitpunkt auf alle bisher eingelesenen 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 Blacklisting und Ausgabe auf Konsole. Die ersten beiden Punkte können durch eine abstrakte Basisklasse oder einen Trait und konkrete Implementierungen gelöst werden. In Listing 4.46 sind die abstrakte Basisklasse Input und die beiden davon abgeleiteten Klassen ReadLineInput und FileInput dargestellt. Listing 4.46: Die abstrakte Klasse Input und die davon abgeleiteten Klassen ReadLineInput 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
4.3 Traits
89
import java.io.File private[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 abgeleitete Klasse ReadLineInput implementiert get durch readline. Die abgeleitete Klasse FileInput definiert einen Klassen-Parameter für den Dateinamen. Der Inhalt der Datei wird dann beim Initialisieren eines FileInput-Objektes mithilfe des Source-Companion-Objektes ausgelesen. Die Methode fromFile benötigt als 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 jedem Zugriff mit next eine Zeile aus der Datei liefert. Dies wird dann in der Methode get genutzt. Sind alle Zeilen der Datei „verbraucht”, gibt hasNext den Wert false zurück. In dem Fall ist das Ergebnis von get die leere Zeichenkette. Der dritte Punkt, der optionale Prompt, wird auch sinnvollerweise durch eine weitere abgeleitete Klasse realisiert, in dem Fall abgeleitet von der Klasse ReadLineInput. Die Klasse ReadLineInputPrompt ist in Listing 4.47 wiedergegeben. 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. Die Methode get wird von ReadLineInput geerbt und so redefiniert, dass zuerst der Prompt und ein Leerzeichen ausgegeben und anschließend die Methode get der Basisklasse mit dem Ausdruck super.get() aufgerufen wird. Die verbliebenen Punkte 4 bis 7 aus der Aufzählung auf Seite 88 lassen sich mit dem Ansatz, speziellere Klassen von den bisherigen abzuleiten, natürlich realisieren, aber die Anzahl der Klassen wird sehr groß, nachdem alle denkbaren Kombinationen realisiert werden sollen. Scala bietet mithilfe von Traits einen sehr eleganten Ausweg. Mit Traits können nämlich auch Methoden von Klassen modifiziert werden. Diese Modifikationen können dann hintereinander geschaltet werden. Daher sprechen wir von stackable modifications (stapelbare Modifikationen).
90
4 Reine Objektorientierung
Um Punkt 4 zu implementieren, definieren wir einen Trait Echo, der die getMethode 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 Klasse, nämlich die abstrakte Klasse Input, und zweitens ruft die Methode get mit super.get() die abstrakte Methode get aus Input auf. Dies scheint zunächst wenig Sinn zu machen, da der super-Aufruf dann in der Klasse Input fehlschlagen würde. Durch die Kennzeichnung der Methode als abstract override bekommt das Konstrukt nun aber folgende Bedeutung: Das extends Input besagt, dass der Trait Echo nur in eine Klasse hineingemixt werden kann, wenn diese Klasse zuvor durch einen anderen Trait oder eine Klasse die Methode get implementiert. Damit macht der Aufruf super.get() auch Sinn, denn damit wird die getMethode in der Form aufgerufen, wie sie vor dem Hineinmixen des Traits war. In der Applikation aus Listing 4.49 sind einige Beispiele zur Verdeutlichung enthalten. 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 hineingemixt werden kann. Das folgende Listing zeigt die Ausführung des Objektes
4.3 Traits
91
StackableTraits1 nach der Kompilierung. Die Kommentare ← Eingabe und ← Echo wurden nachträglich eingefügt, um deutlich zu machen, wo der Nutzer etwas eingegeben hat und wo die durch den Echo-Trait veränderte Methode die Eingabe wieder ausgegeben hat. $ scala StackableTraits1 ReadLineInput Hallo ReadLineInput mit Echo Hallo Hallo ReadLineInputPrompt > Hallo ReadLineInputPrompt mit Echo > Hallo Hallo
← Eingabe ← Eingabe ← Echo ← Eingabe ← Eingabe ← Echo
Die nächste geforderte Modifikation ist das Speichern und die Möglichkeit des späteren Abrufens aller Eingaben. Der Trait Collect (siehe Listing 4.50) setzt dies um. Listing 4.50: Der Trait Collect
trait Collect extends Input { import scala.collection.mutable.ListBuffer private[this] var inputs = new ListBuffer[String] def inputList = inputs.toList abstract override def get() = { val input = super.get() inputs += input input } } 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 eingelesene Zeichenkette kann dem ListBuffer mit der Methode += hinzugefügt werden. Auch im Trait Collect muss die Methode get wieder als abstract override gekennzeichnet werden, um damit eine bereits implementierte Methode get abändern zu können. Der Trait Collect kann nun beispielsweise für die Definition der Klasse MyInput wie folgt genutzt werden: class MyInput extends ReadLineInputPrompt(">") with Collect Die Klasse MyInput erweitert die Klasse ReadLineInput und fügt anschließend den Trait Collect hinzu.
92
4 Reine Objektorientierung Listing 4.51: Das StackableTraits2-Objekt
object StackableTraits2 extends Application { val in = new MyInput in.get() in.get() println(in.inputList) } Wird nun die Applikation aus Listing 4.51 übersetzt und gestartet, kann das Ergebnis folgendermaßen aussehen: $ scala StackableTraits2 > Hallo Welt > Hallo Leser List(Hallo Welt, Hallo Leser) Interessant ist nun aber insbesondere die Hintereinanderschaltung der beiden Traits 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 Welt Hallo Welt > Hallo Leser Hallo Leser List(Hallo Welt, Hallo Leser) Die Methode get der Klasse MyInput wird also sowohl durch Echo als auch durch Collect verändert. Die Eingabe wird auf dem Bildschirm wieder ausgegeben und im ListBuffer gespeichert. Die letzte Variante, die wir implementieren wollen, ist der Blacklist-Trait (siehe 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)) "******" else input } }
4.3 Traits
93
Der Trait Blacklist deklariert zunächst einen abstrakten val mit dem Bezeichner blacklist als Liste von Strings. Die abgeänderte Methode get verhält sich so, dass für eine eingelesene Zeichenkette überprüft wird, ob sie in der Blacklist enthalten ist. Ist dies der Fall, wird die Zeichenkette, wie auf Seite 88 unter Punkt 6 vorgegeben, durch sieben Sternchen ersetzt. Mit der Methode trim werden vorher 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-Objekt und führen es aus, so passiert Folgendes: $ scala StackableTraits3 BEC> Hello World ****** List(******) CBE> Hello World ****** List(Hello World) Bei der ersten Eingabe wurde Hello World sowohl zur Ausgabe als auch in der Liste durch Sternchen ersetzt. Bei der zweiten Eingabe aber nur zur Ausgabe. In der Liste steht die unveränderte Eingabe. An diesem Beispiel ist sehr schön zu sehen, dass die Reihenfolge wichtig ist, in der die Modifikationen angewendet werden. Die get-Methode von inBCE in Listing 4.53 wurde zuerst durch den Trait Blacklist, dann durch Echo und zum Schluss durch Collect modifiziert. Das heißt, zuerst wird die Zeichenkette Hello World durch die Sternchen ersetzt, dann ausgegeben und dann in die Liste gespeichert. Die Reihenfolge bei inCBE ist eine andere. Dort wird Hello World zuerst in die Liste gespeichert, dann durch Sternchen ersetzt und abschließend ausgege-
94
4 Reine Objektorientierung
ben. Durch die Veränderung der Reihenfolge zu with Echo with Blacklist with Collect könnte beispielsweise die Zeichenkette erst unverändert ausgegeben 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 anonymen Klasse erzeugt, das die Klasse ReadLineInputPrompt erweitert. Die Klasse ReadLineInputPrompt wiederum erweitert ReadLineInput und diese die abstrakte Klasse Input. Außerdem werden die drei Traits Collect, Blacklist und Echo hineingemixt. Nachdem alle drei Traits und die Klassen ReadLineInput und ReadLineInputPrompt die Methode get definieren, stellt sich die Frage, welche Methode durch den Ausdruck inCBE.get() ausgeführt wird. Bei Klassen, die einfach voneinander erben, ist die Frage einfach zu beantworten. Interessant ist aber, wie sich die Traits einordnen, zumal ja auch deren get-Methoden allesamt einen super-Aufruf enthalten. Die Antwort von Scala auf diese Frage ist die Linearisierung. Durch Linearisierung werden alle Klassen und Traits in eine wohldefinierte, lineare Ordnung gebracht. Ein super-Aufruf mündet dann einfach in die Methode der nächsten Klasse oder des nächsten Traits in der linearen Kette. Die lineare Ordnung wird von hinten nach vorne berechnet. Die letzten Glieder in der Kette sind die Basisklasse und deren lineare Ordnung. In unserem Beispiel ist 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 der Kette der Traits weggelassen, d.h. durch with Collect wird die Kette erweitert 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 aufgerufen. Diese ruft als Erstes mit super.get() die Methode aus Blacklist auf. Diese wiederum get aus Collect usw. Damit wird klar, dass der eingegebene Wert Hello World zuerst in der Liste landet und erst nach Rückkehr der Methode aus Collect durch Sternchen ersetzt wird.
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 in diesem Fall sowohl ausgegeben als auch in der Liste gespeichert. Durch Veränderung der Reihenfolge, in der die Traits hineingemixt werden, ist es so also auch möglich, die unveränderte Eingabe auszugeben, aber als Sternchen zu speichern.
4.4
Implicits und Rich-Wrapper
Scala verwendet eine Reihe von Java-Typen. Beispielsweise hat die Zeichenkette den Typ java.lang.String. In Scala kann ein Teil von einer Zeichenkette mit der Methode drop zurückgeben werden, z.B. scala> val str = "Hello World" str: java.lang.String = Hello World scala> str drop 6 res0: String = World Beim Blick in die Java-API-Dokumentation fällt aber auf, dass eine Methode drop in der Klasse java.lang.String gar nicht definiert ist. Bei einer Suche in der Scala-API-Dokumentation finden wir die Methode drop in der Klasse StringOps. Allerdings handelt es sich bei dem Typ von str um keinen StringOps.18 Die Lösung liegt in den sogenannten impliziten Umwandlungen, kurz Implicits. Damit kann ein Objekt bei Bedarf automatisch in ein anderes Objekt einer anderen Klasse umgewandelt werden. Durch diesen Ansatz ist es in Scala möglich, bestehende Klassen mit zusätzlichen Methoden anzureichern. Die „anderen” Klassen werden daher auch Rich-Wrapper genannt. Im Beispiel muss also das String-Objekt automatisch in ein StringOps-Objekt umgewandelt werden, um die Methode drop anwenden zu können. Die Methode drop gibt als Ergebnis wieder einen String zurück.
18
Vor Scala 2.8 wurde ein String in einen RichString umgewandelt. In der Klasse RichString waren 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. Zusätzlich zu StringOps gibt es in Scala 2.8 die Klasse WrappedString, deren Methoden dann auch einen WrappedString zurückliefern.
96
4 Reine Objektorientierung
Die automatische Umwandlung funktioniert mit ganz normalen Funktionen, die für den Compiler mit implicit markiert werden. Für unser Beispiel ist das die Methode implicit def augmentString(x: String): StringOps aus dem Objekt PreDef. Das heißt also, beim Ausdruck str drop 6 wird die Zeichenkette Hello World durch augmentString in ein StringOps-Objekt umgewandelt. Dieses versteht dann die Nachricht drop mit dem Argument 6 und gibt als Ergebnis den Wert World vom Typ String zurück. Die Umwandlungsfunktionen werden vor dem Übersetzen durch den Compiler in 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 verhindern, die sonst auftreten würden. Es gibt drei Stellen, an denen Implicits genutzt werden: 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 in einen 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 dem Schlüsselwort implicit markiert sind. Die Definition muss als einfacher Bezeichner sichtbar, d.h. im Scope, sein. Sind z.B. in einem Objekt Sample implizite Umwandlungsfunktionen enthalten, werden sie nach der Import-Klausel import Sample._ als einfache Bezeichner eingeblendet und anwendbar. Außerdem sucht der Compiler in den Companion-Objekten des Quell- und Zieltyps. Diese Umwandlungen müssen nicht als einfacher Bezeichner sichtbar sein.
4.4 Implicits und Rich-Wrapper
97
Die Umwandlung muss eindeutig sein. Gibt es mehrere Funktionen, die den Typfehler berichtigen könnten, wird keine von beiden angewendet, sondern der Typfehler gemeldet. Es wird nur eine implizite Umwandlung an einer Stelle eingesetzt. Wenn ein Typ A vorliegt und B benötigt wird, wird nur nach direkten Umwandlungen von A nach B gesucht. Gibt es eine Umwandlung von A nach C und eine weitere von C nach B, werden die beiden nicht automatisch hintereinander angewendet. Implizite Umwandlungen können den Code an vielen Stellen vereinfachen. Wir wollen dies mit einem Beispiel verdeutlichen. In Listing 4.54 werden zwei Klassen definiert, 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öglichkeit wäre, die Klasse Band um eine Methode addRecord(title: String) zu erweitern. 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 ein Record-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")
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 kann ja beispielsweise durch Map("A" -> "Augsburg", "B" -> "Berlin") erzeugt werden. Diese Notation ist keine spezielle Syntax. Die Zeichenkette vor dem Pfeil wird in eine ArrowAssoc umgewandelt, und deren Methode -> gibt ein Tupel zurück, das in die Map eingefügt werden kann. Eine weitere Anwendung für implicit sind die impliziten Parameter, d.h. Parameter einer Funktion, die beim Aufruf nicht angegeben werden müssen, sondern durch den Compiler ergänzt werden. Zur Veranschaulichung wollen wir auf das Input-Beispiel aus Abschnitt 4.3.2 zur Eingabe unserer Plattensammlung aufbauen. Zur Vereinfachung ist die Plattensammlung eine Map, bestehend aus dem Namen der Band und der Liste der Plattennamen. 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 kleinen Vorgeschmack auf das Pattern Matching, das in Abschnitt 5.4 erläutert wird. Der Code in diesem Beispiel macht Folgendes: Mit der Map-Methode get wird der Wert zum Schlüssel bandname ermittelt. Ist kein entsprechender Eintrag in der Map, wird None zurückgegeben. In diesem Fall wird das Tupel aus dem Bandnamen und der Liste mit dem Plattennamen erzeugt. Ist bereits ein Eintrag vorhanden, wird die Plattenliste rs als Some(rs) zurückgegeben. In diesem Fall wird mit dem Bandnamen und der vorher vorhandenen Liste ergänzt um den eingegebenen Plattennamen ein neues Tupel erzeugt. Der jeweils erzeugte Eintrag landet schließlich mit += in der Plattensammlung.
4.4 Implicits und Rich-Wrapper
99
Über den Parameter input kann die Eingabe entsprechend dem in Abschnitt 4.3.2 vorgestellten Ansatz konfiguriert werden. Beispielsweise kann dann mit folgenden Zeilen eine Plattensammlung erzeugt und mit einem Prompt auf einer neuen Zeile Band- und Plattenname abgefragt werden: val myRecords = new RecordLibrary myRecords.addInteractive(new ReadLineInputPrompt("\n>")) Nachteil dieses Ansatzes ist, dass bei jedem erneuten Aufruf von addInteractive immer wieder das Input-Objekt übergeben werden muss. Eine Vereinfachung bieten die impliziten Parameter. Der Parameter von addInteractive muss 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üsselwort implicit zu versehen. Auch hier gilt wieder, dass die Variable als einfacher Bezeichner sichtbar sein muss. Beispielsweise kann also nach Definition von implicit val myPreferredInput = new ReadLineInput und gegebenenfalls entsprechendem Import-Statement die Methode addInteractive ohne Parameter aufgerufen werden. Implizite Parameter sind nicht eingeschränkt auf einen einzelnen Parameter, sondern das Schlüsselwort implicit bezieht sich auf die gesamte Parameterliste. Um implizite und explizite Parameter zusammen zu nutzen, müssen wir zunächst wissen, dass es in Scala möglich ist, Funktionen mit mehr als einer Parameterliste zu definieren. Das zugrunde liegende Konzept Currysierung kommt aus der funktionalen Programmierung und wird daher erst in Kapitel 5 erklärt. Wir können addInteractive beispielsweise so verändern, dass wir eine explizite 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 {
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 überschrieben werden soll oder nicht. Nachdem ein Default-Wert dafür angegeben ist, kann der Parameter auch weggelassen werden. Er kann aber nicht implizit übergeben werden, sondern ist durch die Definition bestimmt. Das heißt, der Programmierer der Methode gibt vor, was passiert, wenn kein Argument für den Parameter vorhanden ist. Die implizite Parameterliste enthält zwei Einträge: ein Input-Objekt für die Bandeingabe und eines für die Plattentiteleingabe. Nachdem der Compiler die impliziten Parameter nur nach dem Typ auswählt, ist es sinnvoll, möglichst seltene Typen zu nutzen. Wären die beiden impliziten Parameter vom Typ Input, so könnte nicht unterschieden werden. Daher haben wir hier zwei einfache WrapperKlassen BandnameInput und RecordtitleInput definiert. Damit definiert der Nutzer der RecordLibrary-Klasse, was passiert, wenn kein Argument für die implizite Parameterliste übergeben wird. Definieren wir uns nun zwei passende Werte, wie zum Beispiel implicit val bandInput = new BandnameInput(new ReadLineInput) implicit val recordInput = new RecordtitleInput(new ReadLineInputPrompt(">")) können wir die Methode durch den Ausdruck myRecords.addInteractive() nutzen. Damit haben Sie nun alles kennengelernt, was eher dem Bereich objektorientierte Programmierung zuzuordnen ist. Im nächsten Kapitel fahren wir mit den Elementen der funktionalen Programmierung fort.
Kapitel 5
Funktionales Programmieren Scala verbindet als Hybridsprache die objektorientierte Programmierung mit der funktionalen Programmierung1 . Unter objektorientierter Programmierung können sich die meisten etwas Konkretes vorstellen. Was aber ist eine funktionale Programmiersprache? Ob eine Programmiersprache das Label „funktional” führen darf, wurde und wird viel diskutiert. In jüngster Zeit entbrannte auch eine Diskussion, 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 eine postfunktionale Sprache. Allein schon für die Frage, was eine funktionale Programmiersprache überhaupt ist, gibt es verschiedene Antworten. Einigkeit herrscht über den Kern der funktionalen Programmierung: die Berechnung durch Auswertung mathematischer Funktionen und die Vermeidung von Zustand und veränderbaren Daten. Das heißt, wenn wir in Scala val statt var nutzen, sind wir schon ein bisschen funktionaler. Mit funktionaler Programmierung gehen eine Vielzahl von Konzepten einher, deren Umsetzung in Scala Gegenstand dieses Kapitels sind. Im ersten Abschnitt stellen wir Ihnen die Bedarfsauswertung vor, mit der die Berechnung eines vals erst dann durchgeführt wird, wenn das erste Mal auf ihn zugegriffen wird. In Abschnitt 5.2 werden Funktionen und Rekursionen besprochen. Ein wesentliches Merkmal funktionaler Programmierung sind die Funktionen höherer Ordnung (siehe Abschnitt 5.3) , die eine Funktion als Argument oder Ergebnis haben können. Viele funktionale Programmiersprachen unterstützen Pattern Matching, eine Verallgemeinerung des switch-case-Konstrukts. Pattern Matching und die sogenannten Case-Klassen führen wir in Abschnitt 5.4 ein. Das auf den amerikanischen Mathematiker und Logiker Haskell B. Curry zurückgehende Konzept 1
siehe auch [Bir98], [Hug89], [PH06], [OSG08] und [Oka99]
102
5 Funktionales Programmieren
der Currysierung ermöglicht es, in Scala eigene Kontrollstrukturen zu definieren (siehe Abschnitt 5.5). Anschließend werden wir in Abschnitt 5.6 noch einmal zum Schlüsselwort for zurückkommen, um Ihnen zu zeigen, dass mehr als eine Schleife dahintersteckt. Zu guter Letzt werfen wir in Abschnitt 5.7 noch einen Blick auf das sehr ausgefeilte Typsystem von Scala.
5.1
Lazy Evaluation
Standardmäßig werden in Scala alle Felder einer Klasse beim Erzeugen eines Objektes berechnet. Wir sprechen dabei von der sogenannten eager evaluation. In einigen Fällen ist es aber nicht unbedingt sinnvoll, aufwendige Berechnungen, deren Ergebnis vielleicht sogar während der gesamten Existenz eines Objektes nicht benötigt werden, sofort durchzuführen. Der Schlüssel dazu ist die lazy evaluation, die auch mit Bedarfsauswertung übersetzt werden kann. Damit wird der Wert eines Feldes genau dann berechnet, wenn zum ersten Mal darauf zugegriffen wird. Im Gegensatz zu einer Methode wird dieser Wert dann aber in ausgewerteter Form gespeichert. Das heißt, bei einem erneuten Zugriff auf das Feld muss nicht erneut berechnet werden. Scala legt dafür den Modifier lazy fest, der sinnvollerweise nur für vals zulässig ist. 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: 12 x: Int = 12 scala> lazy val y = { | print("Geben Sie eine Zahl ein: ") | readInt | } y: Int = scala> print(y) Geben Sie eine Zahl ein: 13 13 scala> print(x) 12 scala> print(y) 13
5.1 Lazy Evaluation
103
In Listing 4.43 auf Seite 86 haben wir den Trait PositiveNumber definiert. Um Ihnen das Zurückblättern zu ersparen, wollen wir ihn hier noch einmal angeben: trait PositiveNumber { val value: Int require(value>0) } Beim Erzeugen einer anonymen Klasse stellten wir fest, dass ein val in einer Subklasse erst nach dem Aufruf des Konstruktors der Basisklasse initialisiert wird. Gelöst hatten wir das Problem mit den vordefinierten Feldern. Eine zweite Möglichkeit, dies zu lösen, sind lazy vals. Allerdings können nur konkrete vals lazy sein. Daher ist es nicht möglich, den val value mit dem Modifier lazy zu versehen. Listing 5.1: Der Trait PositiveNumber mit einer lazy val
trait PositiveNumber { val inValue: Int lazy val value = { require(inValue>0) inValue } } Die in Listing 5.1 zugegebenermaßen etwas umständliche Version des PositiveNumber-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 inValue nun erfolgreich erzeugt werden kann. Die durch require ausgelöste Exception wird dann erst beim ersten Zugriff auf value geworfen. Die folgende Beispielsitzung soll dies verdeutlichen: scala> val m = new PositiveNumber { | val inValue = 0 | } m: java.lang.Object with PositiveNumber = $anon$1@2754de0b scala> m.value java.lang.IllegalArgumentException: requirement failed at scala.Predef$.require(Predef.scala:134) ...
104
5 Funktionales Programmieren
Damit ein lazy val x Sinn macht, müssen auch alle anderen vals, die in ihrer Berechnung auf x zugreifen, lazy sein. Ansonsten würde der Wert von x sofort benötigt und damit berechnet. Nachdem vars nicht lazy sein können, sollten diese den Wert von x nicht für ihre Berechnungen benötigen oder x sollte nicht lazy sein.
5.2
Funktionen und Rekursionen
Ein wesentliches Merkmal funktionaler Programmierung ist die Unterstützung von Funktionen als first class values. Das heißt, Funktionen sind Werte und können als solche genau wie Zahlen oder Zeichenketten zum Beispiel in Listen gespeichert werden oder sogar Parameter oder Ergebnisse von anderen Funktionen sein. Funktionen können nicht nur wie bisher besprochen als Funktionsgleichung, sondern 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 definiert wird, 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 Zahl anwenden: scala> val inc = (i: Int) => i + 1 inc: (Int) => Int = scala> inc(2) res0: Int = 3 Wir sehen auch in obiger Sitzung den Typ der Funktion (Int) => Int, also eine Funktion mit einer Parameterliste, die einen Int enthält, und dem Ergebnistyp Int. Für Funktionen mit 0 bis 22 Parametern sind in der Scala-Distribution Traits definiert. Die Ausgabe hinter dem Typ ist das Ergebnis der toString-Methode des Traits Function1. Das heißt auch insbesondere, dass jede 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 Der Ansatz kommt aus dem Lambda-Kalkül, in dem diese Funktion als λx.x + 1 geschrieben wird. Siehe auch [Pie02].
2
5.2 Funktionen und Rekursionen
105
Die Variable i ist ein formaler Parameter, aber die Variable a ist eine Referenz auf eine Variable im umschließenden Gültigkeitsbereich. Daher muss eine Variable mit dem Bezeichner a bei der Definition von adder sichtbar sein. Der Compiler 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 der aktuelle Wert von a, sondern eine Referenz auf die Variable in die Closure eingebunden. Damit wirken sich Veränderungen von a nach Definition von adder unmittelbar aus, wie die folgende Sitzung veranschaulichen soll: scala> var a = 10 a: Int = 10 scala> val adder = (i: Int) => i + a adder: (Int) => Int = scala> adder(2) res0: Int = 12 scala> a = 20 a: Int = 20 scala> adder(2) res1: Int = 22 Die Variable a hat zunächst den Wert 10. Folglich wird die Anwendung der Funktion 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änderlichen Variablen gibt, also nur vals, ist es, wenn wir rein funktional programmieren, nicht möglich, durch Schleifen Werte zu berechnen. Beispielsweise ist der Code in Listing 5.2 zur Berechnung der Summe der in der Liste list enthaltenen Zahlen imperativ, da er die Berechnung durch Änderung des Zustands, hier dem Wert der Variablen sum, durchführt. Listing 5.2: Imperative Berechnung der Summe der Zahlen einer Liste
def sum(list: List[Int]) = { var sum = 0 for (elem import scala.annotation.tailrec import scala.annotation.tailrec scala> class Tailrec { | @tailrec def g(i: Int):Int = if (i==0) 0 | else g(i-1) | } :7: error: could not optimize @tailrec annotated method: it is neither private nor final so can be overridden @tailrec def g(i: Int):Int = if (i==0) 0 Wie aus der Fehlermeldung ersichtlich ist, wird eine Methode, die in einer Subklasse redefiniert werden kann, nicht optimiert. Der Kern des Problems rekursiver Funktionen ist, das der Stack mit jedem Funktionsaufruf wächst, was letztendlich je nach Speichergröße und Anzahl der Rekursionschritte zu einem Überlauf und damit einem Absturz des Programmes führen kann. Ein Mittel, dies zu umgehen, sind sogenannte Trampolines4 . Dabei werden die Funktionen so implementiert, dass sie statt eines Wertes eine Funktion zurückgeben, mit der der nächste Schritt berechnet werden kann. Das heißt, der rekursive Aufruf findet nicht in der Funktion selbst statt, sondern die Trampoline4
Die Funktionen prallen wie von einem Trampolin zurück – daher der Name.
108
5 Funktionales Programmieren
Funktion führt einen Schritt nach dem anderen aus. Mit Scala 2.8.0 werden solche Trampoline-Funktionen durch das Objekt scala.util.control.TailCalls unmittelbar unterstützt. Interessant ist Trampolining insbesondere bei wechselseitiger Rekursion, also wenn zwei oder mehr Funktionen sich gegenseitig aufrufen, was nicht automatisch optimiert werden kann. Der Klassiker sind die beiden Funktion isEven und isOdd, die berechnen, ob eine Zahl gerade bzw. ungerade ist und die (wie aus Listing 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 einem Testrechner bei isEven(100000) zu einem StackOverflowError. Durch den Trampoline-Ansatz, der in Listing 5.7 dargestellt ist, wird zwar immer noch für jede Zahl zwischen der eingegebenen und 0 die Funktion isEven oder isOdd aufgerufen, aber jetzt hintereinander. Listing 5.7: Wechselseitig-rekursive Funktionen isEven und isOdd unter Verwendung des 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ür den Rekursionsschritt die Funktion tailcall. Der Ausdruck isEven(100000) gibt damit eine Funktion zurück, die mit result ausgewertet werden kann. Der Ausdruck isEven(100000).result berechnet ohne Stack Overflow den Wert true.
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 Folge daraus, dass Funktionen first class values sind, also gleichberechtigt neben anderen 5 Durch die wechselseitige Abhängigkeit lassen sich die beiden Funktionen nicht nacheinander in die Scala-Shell eingeben.
5.3 Higher-Order-Functions
109
Werten stehen. Funktionen höherer Ordnung sind ein sehr praktisches Mittel zur Abstraktion, wie wir Ihnen im folgenden Beispiel zeigen wollen. Betrachten wir eine Funktion incList, die alle Zahlen in einer Liste inkrementiert. Wir könnten diese Funktion rekursiv definieren, beispielsweise wie in Listing 5.8 angegeben. Listing 5.8: Die Funktion incList zum rekursiven Inkrementieren aller Elemente einer Liste
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 einer Liste verdoppelt. Die entsprechende Funktion ist in Listing 5.9 dargestellt. Listing 5.9: Die Funktion doubleList zum rekursiven Verdoppeln aller Elemente einer Liste
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 sehr wenig. Wo bei incList der Ausdruck list.head+1 steht, heißt es bei doubleList stattdessen list.head*2, und natürlich steht beim rekursiven Aufruf einmal incList und einmal doubleList. Definieren wir zwei Funktionen: def inc(x: Int) = x + 1 def double(x: Int) = x * 2 und nutzen diese, wie in Listing 5.10 dargestellt, sehen die Funktionen sich noch ein bisschen ähnlicher. Listing 5.10: Die Funktionen incList und doubleList mit Nutzung der Funktionen inc 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) }
110
5 Funktionales Programmieren
Mit einer Funktion höherer Ordnung könnten wir die Funktion inc bzw. double als Argument übergeben. Das heißt, statt der beiden nahezu identischen Funktionen incList und doubleList können wir die Funktion funList (siehe Listing 5.11) definieren, die als Parameter eine Funktion erwartet, mit der die Listenelemente verändert werden sollen. Listing 5.11: Die Funktionen funList mit einer Funktion zur Veränderung der einzelnen Elemente 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, die Funktionen 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 Methode bereits zur Verfügung gestellt. Statt funList selbst zu definieren, können wir die Higher-Order-Function map nutzen: def incList (list: List[Int]) = list map inc def 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 Ausdruck rechts 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 dem Unterstrich definieren wir eine sogenannte partiell angewandte Funktion (partially applied function). Dies funktioniert auch mit mehreren Variablen und mehreren Unterstrichen. Beispielsweise definieren wir durch scala> val add = (_: Int)+(_: Int) add: (Int, Int) => Int = eine Funktion add, die eine Parameterliste mit zwei Argumenten erwartet. Der Typinferenzmechanismus kann für die beiden Argumente von add keinen Typ
5.3 Higher-Order-Functions
111
ermitteln, sodass wir Typinformationen hinzufügen müssen. Gäben wir keinen Typ an, sähe das in der Scala-Shell folgendermaßen aus: scala> val add = _+_ :5: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$plus(x$2)) val add = _+_ ^ :5: error: missing parameter type for expanded function ((x$1: , x$2) => x$1.$plus(x$2)) val add = _+_ ^ Die korrekt definierte Funktion add kann auf eine Argumentliste, bestehend aus zwei 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 Nachricht apply mit einer Argumentliste, die zwei Ints enthält. In der funktionalen Programmierung dominiert die Liste als Datenstruktur. Für sie sind in der Regel eine Vielzahl von Funktionen höherer Ordnung definiert, um die Liste in eine neue zu transformieren. Das neue Collection-Framework (siehe Abschnitt 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 übergebenen Funktion verändert und die neue Liste zurückgibt, gibt es auch die Funktion foreach, die eine Funktion vom Typ A => Unit als Argument hat und diese mit allen Elementen ausführt. Beispielsweise können mit list.foreach(e => println(e)) alle Elemente einer Liste list zeilenweise ausgegeben werden. Auch hier kann die gebundene Variable e wieder durch den Unterstrich ersetzt werden: list.foreach(println(_)) Statt println(_) kann auch println _6 geschrieben werden. Damit steht der Platzhalter nicht mehr für den einen Parameter, sondern für die gesamte Parameterliste, die in diesem Fall einen Parameter enthält. Wird im Kontext eine Funktion erwartet, kann ein Platzhalter für die gesamte Parameterliste sogar weggelassen werden, wie beispielsweise bei list.foreach(println)
6 Es ist wichtig, zwischen println und dem Unterstrich ein Leerzeichen einzufügen. Zusammengeschrieben, also println_, ist es ein gültiger Bezeichner.
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 Funktionen mit dem Ergebnistyp Boolean, als Parameter. Damit lässt sich beispielsweise ein Teil einer Liste ermitteln, für den ein Prädikat gilt oder nicht. Mit list filter (_ 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 Elemente der 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 (_ list exists (_ ("Anfang -> " /: list)(_+_) res6: java.lang.String = Anfang -> 12345 scala> (list :\ " ")(_+_) res8: java.lang.String = Anfang -> 12345 scala> (list foldRight " ")(_+_) res10: List[java.lang.String] = List(Anfang -> , Anfang -> 1, Anfang -> 12, Anfang -> 123, Anfang -> 1234, Anfang -> 12345)
114
5 Funktionales Programmieren
scala> (list scanRight " LiftFilter Lift Filter The Filter that intercepts lift calls net.liftweb.http.LiftFilter bootloader org.obraun.talkallocator.Boot LiftFilter /*
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 ScalaFunktionen, sogenannte Snippets, eingebunden. Daneben gibt es noch die Möglichkeit, ohne Templates nur mithilfe von Scala-Funktionen, sogenannten Views, zu rendern. Wir beschränken uns im Folgenden auf die Verwendung von Templates und Snippets. Wird http://localhost:8080/ aufgerufen, wird das Template mit dem Namen default.html aus dem Verzeichnis templates-hidden unter wepapp gerendert und ausgeliefert8 . Ein Blick in die Datei zeigt HTML mit einigen Besonderheiten. Was wir ganz einfach anpassen können, ist der Titel der Seite, da es sich dabei um das übliche HTML-Tag title im head handelt. Wir ersetzen das bestehende Tag also durch: Talk Allocator Nach dem Titel sehen wir schon zwei Lift-spezifische Tags: Dabei handelt es sich um die Einbindung zweier Snippets. Die Tags haben die Form , wobei der snippet_name eine Methode einer Klasse oder eines Objekts ist. In den oben angegebenen Snippets sind das die Methoden blueprint und fancyType des CSS-Objekts des Lift-Frameworks. Die Bedeutung des Snippet-Tags ist, die entsprechende Methode auszuführen und den Tag durch das Ergebnis zu ersetzen. Das heißt natürlich, dass die Methoden einen Wert vom Typ scala.xml.NodeSeq zurückgeben müssen. Im Body des Templates können wir noch die Überschrift von app zu Talk Allocator ändern. Etwas weiter unten in der Datei sehen wir noch zwei solcher Snippet-Tags: Durch das erste Tag wird an dieser Stelle die Methode builder des Objekts Menu ausgeführt. Diese rendert das Menü mit dem SiteMap-Inhalt. Im zweiten Snippet-Tag steht keine Methode. Wird keine Methode angegeben, wird die render-Methode ausgeführt, das heißt also in diesem Fall die render-Methode des Objekts Msgs. Diese gibt Meldungen des Lift-Frameworks wie Fehlermeldungen, Warnungen oder Benachrichtigungen aus. Über das Attribut showAll, das in der render-Methode ausgelesen wird, kann angegeben werden, ob alle Meldungen 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.
9.3 Rendering – Templates und Snippets
263
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 etwas eingefügt werden kann. Und damit kommen wir zum echten Einstiegspunkt in die Applikation. Natürlich, Sie haben sicher richtig vermutet, wird beim Aufruf von http://localhost:8080/ nicht die Datei default.html, sondern index.html ausgeliefert. Die im Lift-Prototyp mitgelieferte Datei index.html im Verzeichnis webapp ist in Listing 9.6 wiedergegeben. Listing 9.6: Die Datei index.html aus dem Lift-Prototyp
Welcome to your project!
Welcome to your Lift app at
Was wir als oberstes XML-Element in der Datei index.html sehen, ist das Gegenstück zum Bind-Tag aus der Datei default.html. Das Surround-Tag nimmt das Template, das mit dem Attribut with spezifiziert ist, und ersetzt das BindTag mit dem Namen content durch den eigenen Inhalt, also von bis zu . Dieser Inhalt enthält ein Snippet-Tag, das die Methode howdy der Klasse HelloWorld aufruft. Diesmal ist der Start- und End-Tag voneinander getrennt, und es steht Folgendes dazwischen: Welcome to your Lift app at Auf diese Weise können wir eine NodeSeq als Argument an ein Snippet übergeben. Das heißt, es gibt Snippets ohne und Snippets mit einer NodeSeq als Argument. Das verwendete Argument ist gültiges XML, enthält aber auch wieder eine Besonderheit: das Tag . Dieses Tag wird in der howdy-Methode ersetzt. Werfen wir dazu einen Blick auf die Methode: import net.liftweb.util.Helpers.bind def howdy(in: NodeSeq): NodeSeq = bind("b", in, "time" -> date.map(d => Text(d.toString))) Durch Verwendung von bind des Objekts Helpers wird durch einen Text-Knoten mit dem aktuellen Datum ersetzt. Die Methode bind erwartet 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.
264
9 Webprogrammierung mit Lift
Ein oder mehrere Mappings von Elementnamen (time) zu dem ersetzenden Element (date.map(d => Text(d.toString)))). Das Feld date ist vom Typ Box[Date]. Wichtig bei dem Feld ist, dass es als lazy val definiert wurde. Dies stellt sicher, dass die aktuelle Zeit erst beim Zugriff ermittelt wird. Nachdem HelloWorld kein Objekt, sondern eine Klasse ist, wird bei 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.7 dargestellt wird. Listing 9.7: Die Datei index.html des Talk Allocators
Willkommen beim Talk Allokator Noch nicht vergebene Talks sind Bereits vergeben sind Wir nutzen das Template default und geben die noch nicht und die bereits vergebenen Talks aus. Dazu haben wir zwei Snippet-Tags angegeben. Damit dies funktioniert, müssen wir eine Klasse oder ein Objekt Talks mit den beiden Methoden available und allocated implementieren. Befinden muss sich das Ganze dann im Sub-Package snippet eines dem Lift-Framework im Rahmen des Bootstrapping bekannt gemachten Packages. Nachdem wir in der Boot-Klasse das Package org.obraun.talkallocator zu den LiftRules hinzugefügt haben, speichern wir unsere Snippets in org.obraun.talkallocator.snippet. Bevor wir dies tun, beschäftigen wir uns jedoch in den nächsten beiden Abschnitten erst einmal mit der mitgelieferten Benutzerverwaltung, der SiteMap und dem Lift-eigenen OR-Mapper9 .
9.4
Benutzerverwaltung und SiteMap
Das Lift-Framework enthält eine Benutzerverwaltung, die wir bereits im Prototyp gesehen haben. Lift bietet zur Verwaltung eines Benutzers den Trait MegaProtoUser. Im Folgenden passen wir die Datei User aus dem Prototyp an unsere Bedü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 verwendete Datei User.scala ist in Listing 9.8 wiedergegeben.
9
Objektrelationale Abbildung, zur Speicherung von Objekten in einer relationalen Datenbank.
9.4 Benutzerverwaltung und SiteMap
265
Listing 9.8: Die Klasse User mit Companion-Objekt für die Benutzerverwaltung
package org.obraun.talkallocator package model import 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( ) override def skipEmailValidation = true } Die Klasse User fügt das MegaProtoUser-Trait hinzu. Dieses Trait definiert einen User, der unter Verwendung des OR-Mappers gespeichert werden kann. Die einzige 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 Companion-Objekt. Durch Hineinmixen des Traits MetaMegaProtoUser zum Objekt User haben wir die gesamte benötigte Funktionalität. Wir übernehmen auch hier einige Redefinitionen aus dem Lift-Prototyp. Wir geben einen eigenen Tabellennamen für die Speicherung der Benutzer an. Mit der Methode screenWrap wird die Darstellung der Webseiten der Benutzerverwaltung spezifiziert, und schließlich lassen wir 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 die SiteMap. 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)),
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 Benutzer eingeloggt 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ätzlich eine Seite, um neue Talks anzulegen und Talks zu löschen. Außerdem soll die Standard-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 Name und Linktext das übergebene Argument bekommt. Dem Ergebnis fügen wir mit der /-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-Text und einen LocParam. Ein Link wird als Liste von Verzeichnissen repräsentiert, also müssten wir für den Link admin/add List("admin","add") angeben. Ein LocParam schließlich modifiziert einen Eintrag. In unserem Beispiel haben wir die beiden LocParams, ifLoggedIn und ifAdmin, der Übersichtlichkeit halber als vals definiert. Der in beiden Fällen verwendete LocParam ist ein Objekt der Klasse If. Der erste Parameter ist ein Prädikat, also eine Funktion, die einen Boolean zurückgibt. Der zweite Parameter spezifiziert das Verhalten, das ausgeführt werden soll, wenn das Prädikat nicht zutrifft. Die so implementierte Liste konkatenieren wir mit der User.sitemap. Schließlich nutzen wir die Elemente der Liste entries einzeln, deshalb als entries:_*, zur Erzeugung eines SiteMap-Objekts und übergeben dieses an die Methode setSiteMap des LiftRules-Objekt.
9.5
Persistenz
Als Nächstes implementieren wir eine Klasse Talk für die Talks, die in der Datenbank über den OR-Mapper persistiert werden sollen. Ein Talk soll dabei ein Feld für einen Titel und ein Feld für den Vortragenden haben. Ohne OR-Mapping wü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 ein kleines bisschen aufblähen, wie in Listing 9.10 dargestellt ist.
9.5 Persistenz
267 Listing 9.10: Die Klasse Talk
class Talk extends LongKeyedMapper[Talk] with IdPK { def getSingleton = Talk object 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 Klasse Talk hineinmixen. Wir wählen den LongKeyedMapper-Trait, der einen LongWert als Primary-Key in der Datenbank verwendet. Nachdem wir den PrimaryKey nicht selbst definieren wollen, fügen wir eine passende Implementierung durch den IdPK-Trait hinzu. Damit bekommt jedes Talk-Objekt einen eindeutigen Schlüssel mit dem Bezeichner id. Wie schon in der Klasse User (siehe Listing 9.8) müssen wir die Methode getSingleton definieren. Analog zu User verwenden wir auch hier das Companion-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 jeweils verwendeten Trait. Den Titel wollen wir als String mit einer maximalen Lä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 zweite Parameter gibt die maximale Länge an. Um den Vortragenden zu verwalten, nutzen wir einen MappedLongForeignKey, also einen Long-Wert der als Fremdschlüssel genutzt wird. Als Argumente übergeben wir wieder das Objekt, dem dieses 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 LongKeyedMetaMapper hinzu. Analog zur Benutzerverwaltung haben wir damit die für den ORMapper benötigte Funktionalität und können diese nach Bedarf abändern. Wir geben der Tabelle, in der die Talks gespeichert werden, den Namen talks. Damit können wir in unserer Webapplikation Talk-Objekte nutzen, die in der Datenbank gespeichert werden. Der Zugriff auf die Felder der Talk-Objekte ist dabei ganz normal möglich. Wenn wir beispielsweise ein Talk-Objekt mit dem Bezeichner myTalk haben, können wir mit myTalk.title auf den Titel zugreifen. Das Erzeugen, Verändern und Speichern von Objekten funktioniert aber etwas anders als sonst üblich. Ein Talk-Objekt erzeugen wir mit der Methode create des Talk-CompanionObjekts. Einen Wert für ein Feld setzen wir über die apply-Methode des das Feld
268
9 Webprogrammierung mit Lift
repräsentierende Objekts. Durch den folgenden Ausdruck erzeugen wir beispielsweise 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 Methoden des Companion-Objekts. Die Methode find beispielsweise bekommt als Parameter einen QueryParam und gibt eine Box zurück, die entweder leer ist oder das gefundene Objekt enthält. In Listing 9.12 ist die Methode createExampleTalks definiert, die wir zum Objekt Talk hinzufügen. Der in der Methode verwendete QueryParam wird vom Objekt By erzeugt und sucht nach dem Wert talk im Feld 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 beiden Talks bereits in der Datenbank vorhanden sind. Wenn nicht, werden sie neu angelegt. Das jeweils nicht gesetzte Feld speaker hat in der Datenbank den Wert NULL. Der Wert NULL kann auch explizit mit dem Methodenaufruf myTalk.speaker(Empty) gesetzt werden. Um beim Applikationsstart auch gleich zwei Beispielnutzer anzulegen, ergänzen wir das Objekt User um die in Listing 9.13 angegebene Methode createExampleUsers 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)
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 Templates für das Auswählen und das Administrieren der Talks. Das Template für die Startseite haben wir bereits in Listing 9.7 gezeigt. Die Startseite soll dann wie in Abbildung 9.3 gezeigt aussehen.
Abbildung 9.3: Startseite des Talk Allocators
Die beiden benötigten Snippets wollen wir nun als Erstes implementieren. Dazu erzeugen wir eine Datei Talks.scala im Sub-Package snippet des Packages org.obraun.talkallocator. Wir benötigen zwei Methoden, available und allocated, die jeweils eine NodeSeq als Ergebnis haben. Bei den bereits vergebenen Talks soll dahinter in Klammern der Sprecher mit Vor- und Zuname angegeben werden. Da das nur ein kleiner Unterschied ist, implementieren wir eine Methode talksAsTable, die ein Flag als Parameter hat, das angibt, welche Talks, freie oder vergebene, angezeigt werden sollen. Das Objekt Talks mit den drei Methoden ist in Listing 9.14 angegeben.
270
9 Webprogrammierung mit Lift Listing 9.14: Das Objekt Talks mit den ersten Snippets
package org.obraun.talkallocator package snippet import scala.xml._ import import import import import import
net.liftweb._ mapper._ util.Helpers._ common._ http.S._ 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) ).get Text(speaker.firstName+" "+speaker.lastName) } val talks = Talk.findAll( if (available) NullRef(Talk.speaker) else NotNullRef(Talk.speaker) )
{ talks.map{ talk => {talk.title} | {if (!available) ({speaker(talk.speaker)}) | }
} }
} }
9.6 Implementierung der Snippets
271
Die Methode talksAsTable besteht aus drei Teilen: einer Funktion zum Ausgeben des Sprechers, der Berechnung der anzuzeigenden Talks und der Formulierung des Ergebniswertes. Nachdem der Sprecher als Fremdschlüssel im Talk enthalten ist, übergeben wir den Wert an die Funktion speaker. In der Funktion speaker ermitteln wir das dazugehörige User-Objekt mit der Methode User.find. Nachdem find eine Box zurückgibt, packen wir das Objekt mit get aus. Anschließend erzeugen wir aus dem Vor- und Zunamen ein Objekt vom Typ scala.xml.Text. Zum Ermitteln der Talks nutzen wir die Methode findAll. Freie Talks haben in der Datenbank in der Spalte speaker den Wert NULL stehen. Über den QueryParam NullRef können wir so alle freien Talks ermitteln. Umgekehrt finden wir mit NotNullRef alle bereits vergebenen Talks. Die NodeSeq, die talksAsTable schließlich zurückgibt, ist eine Tabelle. Aus jedem Talk wird eine eigene Zeile gemacht, die entweder nur den Titel oder Titel und 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 zum Beispiel wie in Abbildung 9.4 aus. Damit sich Egon User überhaupt einen Talk auswählen kann, müssen wir das Template und das Snippet dafür implementieren. Listing 9.15 zeigt das Template für die Auswahlseite. Aussehen soll die Seite dann wie in Abbildung 9.5. Listing 9.15: Das Template choose.html für die Auswahl eines Talks
Talk auswählen Wie in Listing 9.15 zu sehen ist, wählen wir dieselbe Struktur mit dem defaultTemplate. Das Snippet-Tag hat dieses Mal ein Attribut form. Damit wird das Snippet zum Formular. Implementiert werden soll es als Methode choose im TalksObjekt. Diese Methode soll Folgendes leisten:
272
9 Webprogrammierung mit Lift
Abbildung 9.5: Auswahlseite des Talk Allocators
1. Klickt der Benutzer auf den Button „Keinen Talk übernehmen’,’ wird sichergestellt, dass ihm kein Talk zugeordnet ist. Hatte er bereits einen Talk, wird bei diesem der Sprecher wieder auf NULL gesetzt. 2. Es werden alle verfügbaren Talks als Radio-Button-Liste angezeigt. Hat der Benutzer bereits einen Talk ausgewählt, steht dieser ganz oben und ist bereits markiert. Ist noch keiner ausgewählt gewesen, wird auch keiner vormarkiert. Damit führt ein sofortiges Klicken auf den Button „Auswählen” zu keiner Änderung. 3. Wählt der Benutzer einen Talk aus und bestätigt dies mit dem Button „Auswählen”, wird ihm der entsprechende Talk zugeordnet. Ein vorher gewählter wird wieder freigegeben.
Listing 9.16: Die Methode Talks.choose
def choose = { val val val var
user = User.currentUser.open_! chosen = Talk.findAll(By(Talk.speaker,user.id)) available = Talk.findAll(NullRef(Talk.speaker)) 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) =>
9.6 Implementierung der Snippets
273
if (hasOld) { val old = chosen.head if (old.title != talk.title) { old.speaker(Empty).save talk.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 else Full(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 einige vals und eine var definiert. Mit User.currentUser bekommen wir den eingeloggten Benutzer in einer Box die wir mit open_! auspacken. Der Zugriff mit open_! wirft eine Exception, wenn kein Benutzer eingeloggt ist. Aufgrund unserer SiteMap ist die entsprechende Seite aber nur zu sehen, wenn ein Benutzer eingeloggt ist. Anschließend berechnen wir die Liste der bereits ausgewählten Talks und weisen sie chosen zu. Obwohl nur maximal ein Talk zugewiesen sein kann, wird in solchen Fällen in der funktionalen Programmierung gerne mit Listen gearbeitet. Dies macht beispielsweise auch das Erzeugen des Radio-Buttons weiter unten ein kleines bisschen knapper. Das Ermitteln der verfügbaren Talks kennen wir schon aus der talksAsTable-Methode. Für den eventuell ausgewählten Titel nutzen wir eine lokale Variable, die wir mit None vorbelegen. Die oben beschriebene Logik der Methode lagern wir in die Funktion chooseTalk aus, die den Wert von newTitle übergeben bekommt. Handelt es sich da-
274
9 Webprogrammierung mit Lift
bei um None, wird der eventuell vorher bereits gewählte Talk zurückgesetzt, indem dem speaker-Objekt eine leere Box übergeben und das Talk-Objekt mit save in die Datenbank geschrieben wird. Wurde ein Titel übergeben, wird der Talk ermittelt. Anschließend werden die verschiedenen Fälle abgearbeitet. Am Ende der Funktion chooseTalk wird auf die Startseite umgeleitet. Die dazu genutzte Methode redirectTo gehört zum S-Objekt, das den aktuellen Zustand des HTTP-Request und -Response repräsentiert. Nach der Funktion chooseTalk müssen wir noch das Ergebnis, also die NodeSeq definieren. Eine Gruppe von Radio-Buttons können wir mit der Methode radio des SHtml-Objekts erzeugen. Die drei Parameter sind: 1. Die verschiedenen Optionen, bei uns die verschiedenen Talks. Nachdem der Wert von chosen eine Liste ist, können wir diese, egal ob leer oder nicht, einfach vor die Liste der verfügbaren Talks hängen. 2. Die vorausgewählte Option als Box. Dabei entspricht Empty keiner Vorauswahl. 3. Die Funktion, die mit der gewählten Option ausgeführt wird. Wir weisen die gewählte Option verpackt in ein Some der Variablen newTitle zu. Die so erzeugten Radio-Buttons müssen noch mit toForm in eine NodeSeq umgewandelt werden. Die beiden Buttons definieren wir jeweils mit der Methode submit, die ein Label für den Button und eine Funktion zum Ausführen nach dem Klicken bekommt. In beiden Fällen nutzen wir unsere Hilfsfunktion chooseTalk. Abschließend hängen wir die beiden Buttons noch an die NodeSeq der Radio-Buttons an und geben dies als Ergebnis des Snippets zurück. Damit haben wir die volle Funktionalität zur Auswahl eines Talks in den Talk Allocator implementiert. Was nun noch folgt, ist die Administrationsseite mit den Möglichkeiten, Talks anzulegen 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-Tag und dazwischen ein bisschen XML. Dieses XML dazwischen wird dem Snippet Talks.add als Argument übergeben. Es enthält zwei selbst definierte Tags mit dem Präfix talk. An diesen Stellen werden wir mit der Methode add etwas einfügen. Der zweite Snippet-Tag (zum Löschen) hat kein Argument für das Snippet Talks.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
Neuer Talk
9.6 Implementierung der Snippets
275
|
|
Talk löschen
Abbildung 9.6: Administrationsseite des Talk Allocators
Die Methode add des Talks-Objekts ist in Listing 9.18 dargestellt. Nach einer Variable für den im Textfeld einzugebenden neuen Titel definieren wir die Hilfsfunktion addTalk. Diese erzeugt und speichert einen neuen Talk mit dem übergebenen Titel, außer es handelt sich dabei um den leeren String oder ein Talk mit dem Titel ist bereits vorhanden. Für die Erzeugung der NodeSeq nutzen wir die 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!="" &&
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.17 haben wir für die beiden Tags, die wir ersetzen wollen, den Namespace talk gewählt. Das zweite Argument ist die NodeSeq, in der ersetzt werden soll – in unserem Fall das Argument der Methode add mit dem Namen html. Die weiteren Argumente sind vom Typ BindParam. Der erste davon ersetzt den Tag title im Namespace talk durch ein Textfeld unter Verwendung der Methode SHtml.text. Das erste Argument von text ist der vordefinierte Inhalt. Das zweite Argument, die Funktionalität, weist den eingegeben Text ohne führende und schließende Leerzeichen der Variablen title zu. Der Tag talk:add wird durch einen Button mit dem Label „Hinzufügen”, der die addTalk-Funktion ausführt, ersetzt. Listing 9.19: Die Methode Talks.delete
def delete = { import scala.collection.mutable.Set val 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) :+ ) val delete = submit( "Löschen",
9.6 Implementierung der Snippets
277
() => deleteTalks(toDelete) ) checkboxes ++ delete } Die Methode Talks.delete für das zweite Snippet-Tag des Templates der Administrationsseite ist in Listing 9.19 dargestellt. Durch die Verwendung von Checkboxen 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öschen sollen ausnahmslos alle Talks zur Verfügung stehen, also auch bereits vergebene. Die Hilfsfunktion deleteTalks löscht alle übergebenen Talks. Für jeden Talk erzeugen wir eine Checkbox, die nicht markiert ist. Falls sie markiert wurde, wird der Talk zu toDelete hinzugefügt. Neben der Checkbox muss der Title des Talks noch explizit angegeben werden.10 Den Button zum Löschen erzeugen wir analog zu den bisherigen Buttons. Damit sind wir mit unserer kleinen Lift-Webapplikation fertig. Wir konnten Ihnen im 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 Checkboxen erzeugen könnten. Diese zeigt aber den gesamten Talk und nicht nur den Titel an. Ok, wir könnten natürlich noch toString in der Klasse Talk redefinieren. Sie sehen: Es gibt immer mehrere Möglichkeiten!
Kapitel 10
Leichtgewichtige Webprogrammierung mit Scalatra Neben umfangreichen Webframeworks wie beispielsweise dem in Kapitel 9 vorgestellten Lift gibt es auch leichtgewichtigere Alternativen in Scala. Eines davon ist das vom Ruby-Webframework Sinatra1 inspirierte Scalatra2 , das wir Ihnen in diesem Kapitel anhand eines Beispiels vorstellen wollen. Als einfaches Beispiel dient der Final-Grade-Calculator. Damit soll sich die Gesamtnote für einen der zwei Informatik-Studiengänge Bachelor of Science und Diplom aus drei Teilnoten berechnen lassen. In einer Webapplikation soll es möglich sein, den Studiengang und die drei zur Berechnung notwendigen Teilnoten zu übergeben 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 einem vom Scalatra-Team bereitgestellten Prototyp beginnen. Anschließend erstellen 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 Prototypen, den wir herunterladen und anpassen können. Dazu klonen wir das Git1 2
http://www.sinatrarb.com/ http://www.scalatra.org/
280
10 Leichtgewichtige Webprogrammierung mit Scalatra
Repository, wechseln in das Verzeichnis, laden per Sbt die benötigten Ressourcen herunter 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 update Getting Scala 2.7.7 ... ... $ sbt jetty [info] Building project scalatra-sbt-prototype 0.1.0SNAPSHOT against Scala 2.8.0 ... Öffnen wir anschließend die URL http://localhost:8080/ im Browser, werden wir mit „Hello, world!” begrüßt. Die Scalatra-Webapplikation besteht aus einer 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("/") { Hello, world! } } Mit Scalatra können sowohl Filter als auch Servlets, basierend auf dem JavaPackage javax.servlet, erstellt werden. Der Trait ScalatraFilter erweitert das Java-Interface Filter. Die Klasse ScalatraServlet erweitert die JavaKlasse 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ückgegeben wird. get() – Antwort auf einen GET-Request mit dem Pfad . Beginnt ein Teil des Pfads mit einem Doppelpunkt, wird dieser Teil als Parameter übergeben. Wir werden dies im Abschnitt 10.2 nutzen. post() – Antwort auf einen POST-Request mit dem Pfad . put() – Antwort auf einen PUT-Request mit dem Pfad . delete() – Antwort auf einen DELETE-Request mit dem Pfad .
10.2 Der Final-Grade-Calculator
281
error – Wird bei einem Fehler ausgeführt. after – Wird nach dem passenden get-, post-, put- bzw. delete-Block ausgeführt.
10.2
Der Final-Grade-Calculator
Nachdem wir den Scalatra-Prototypen zum Laufen bekommen haben, bauen wir diesen schrittweise zum Final-Grade-Calculator (FGC) um. Dazu verschieben wir als Erstes die Klasse MyScalatraFilter an die gewünschte Stelle und passen Namen 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("/") { Final-Grade-Calculator } } Damit der Filter auch gefunden wird, müssen wir die Datei web.xml im Verzeichnis src/main/webapp/WEB-INF anpassen. Die gesamte Datei ist in Listing 10.3 zu sehen. Listing 10.3: Die Datei web.xml
scalatra org.obraun.finalgradecalculator.FGCFilter
282
10 Leichtgewichtige Webprogrammierung mit Scalatra
scalatra /* 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 wir mit einer get-Methode, der wir die Parameter in der URL übergeben. Der Pfad soll dazu folgendermaßen aufgebaut sein: /calculate/grade/mark1/mark2/mark3 wobei statt des Studiengangs und der drei Noten die tatsächlichen Werte stehen sollen. 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 zugreifen. Die Reaktion auf ein GET-Request mit solch einer URL ist in Listing 10.4 wiedergegeben. Listing 10.4: Die Methode, um die Note bei einem GET-Request zu berechnen
get("/calculate/:grade/:mark1/:mark2/:mark3") { val val val val
grade mark1 mark2 mark3
= = = =
params("grade") params("mark1") replace (’,’,’.’) toDouble params("mark2") replace (’,’,’.’) toDouble params("mark3") replace (’,’,’.’) toDouble
def isValid(mark: Double) = (1.0 movie) movieOption } } def returnMovie(serial: Int) = { val movie = rent(serial) rent -= serial available += (serial -> movie) } def availableMoviesForAge(age: Int) = available.filter{ case (_,Movie(_,r)) => r None
11.2 Der MovieStore
291
case Some(movie @ Movie(_,r)) => if (r movie) movieOption } else None } } ... } Nach der Definition des Packages importieren wir den Akka-Actor-Trait. Die beiden Maps sollen nun nicht mehr private sein, sondern vererbt werden können. Die Methode rentMovieAge ist neu und löst die Methode rentMovie ab, die das Alter beim Ausleihen nicht beachtete. Die Methoden addToStore, returnMovie und availableMoviesForAge übernehmen wir unverändert. Daher haben wir sie in Listing 11.4 ausgespart. Die beiden Methoden availableMovies 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 Implementierung dieser Methode entspricht dem, was in Scala-Actors mit einem receive- bzw. react-Block gemacht wird. Bevor wir uns mit der Implementierung beschäftigen, definieren wir zunächst einmal die verschiedenen Messages, auf die der MovieStore-Trait reagieren bzw. die er versenden können soll. Die Messages sind in Listing 11.5 dargestellt. Wir werden später noch weitere Messages 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 Message case 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 Message case class Error(msg: String) extends Message Die drei Messages AvailableList, RentMovie und Return sollen vom MovieStore empfangen werden. Die Messages ResultList, SuccessfullyRent und Error werden für Antworten genutzt. Die Reaktion auf die Messages
292
11 Akka – Actors und Software Transactional Memory
definieren wir in der Methode rentalManagement, die im Trait MovieStore dann auch als Implementierung von receive dient. Die beiden Methoden sind in 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 Matching auf die verschiedenen Messages. Etwas Neues ist der Ausdruck self.reply(...). In Akka werden Actors über ActorRefs gesteuert. Das sind Referenzen auf Actors. Das self-Feld eines Actors enthält die Referenz auf sich selbst. Im ActorRef-Trait gibt es dann die Methoden wie ! zum Senden von Messages oder, wie in Listing 11.6 verwendet, reply zum Senden einer Message an den Sender der empfangenen Message. Auf die Message AvailableList wird die Map der verfügbaren, für das Alter erlaubten Filme ermittelt. Diese wird in eine Liste von Tripeln umgewandelt, die aus 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, ohne den Client anpassen zu müssen. Die Liste wird dann in eine ResultListMessage verpackt und mit self.reply zurückgesendet. Bei einer RentMovie-Message wird – je nachdem, ob der Film ausgeliehen werden kann bzw. darf – mit einer SuccessfullyRent- oder Error-Message geant-
11.3 User- und Session-Management
293
wortet. Beim Zurückgeben eines Films über die Message Return ist keine Antwort vorgesehen.
11.3
User- und Session-Management
Als Nächstes wollen wir ein Session-Management implementieren. Dazu benötigen 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.getInstance val age = today.get(Calendar.YEAR) dateOfBirth.get(Calendar.YEAR) today.set(Calendar.YEAR, dateOfBirth.get(Calendar.YEAR)) if (today before dateOfBirth) age-1 else age } } object User { private[this] var id = 0 def apply(name: String, dateOfBirth: Calendar) = { id += 1 new 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 age lä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-
294
11 Akka – Actors und Software Transactional Memory
ne Instanz der Klasse mit new erzeugt werden kann. Stattdessen definieren wir im Companion-Objekt eine apply-Methode, die ein Objekt mit einer eindeutigen ID erzeugt und zurückgibt. Die unapply-Methode definieren wir, damit wir mit User-Objekten Pattern Matching machen können. Für das Session-Management benötigen wir noch einige Messages. Diese sind in Listing 11.8 dargestellt. Mit den Messages Login und Logout kann sich ein Benutzer an- bzw. abmelden. Mit ShowAvailable fragt der Benutzer die Liste der fü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 Message case class Logout(userID: Int) extends Message case class ShowAvailable(userID: Int) extends Message case class Rent(userID: Int, serial: Int) extends Message Die Implementierung des Traits SessionManagement ist in Listing 11.9 angegeben. 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
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.stop sessions -= 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 Feld users entspricht einer Map mit UserIDs als Schlüssel und User-Objekten als Werten. Das Session-Management empfängt im Wesentlichen Login- und LogoutMessages und erzeugt und beendet die dazugehörigen Sessions. Die Implementierung der Sessions werden wir im Anschluss an das Session-Management besprechen. Die gerade existierenden Sessions werden im sessions-Feld in einer Map gespeichert, die den UserIDs die dazugehörige Session als ActorRef zuordnet. Um auf zusätzliche Messages reagieren zu können, wird die Methode receive abstrakt redefiniert. Damit ist der Trait SessionManagement stapelbar (siehe auch Abschnitt 4.3.2 auf Seite 88). Nachdem receive eine partielle Funktion ist, können wir die Methode orElse nutzen. Damit wird auf eine Message die Methode sessionManagement angewendet, wenn sie an der Stelle, d.h. für diese Message, definiert ist. Sonst wird die in der jeweiligen Klasse, die den Trait SessionManagement hinzufügt, definierte Methode receive durch den Ausdruck super.receive genutzt. Das eigentliche Management der Sessions erfolgt in der Methode sessionManagement. Auf die Message Login hin wird versucht, den zur übergebenen id gehörenden User zu ermitteln. Ist dies nicht möglich, wird eine Fehlermeldung zurückgesendet. Ist dies der Fall, wird überprüft, ob für die id bereits eine Session besteht. Nur wenn keine Session gefunden wird, wird als Erstes mit der Methode log aus dem Trait Actor eine Meldung ausgegeben. Wo diese Logmeldung dann einmal landet, kann auf der Akka-Plattform konfiguriert werden. Anschließend wird eine neue Session erzeugt, gestartet und in der sessions-Map gespeichert.
296
11 Akka – Actors und Software Transactional Memory
Eine Session ist auch ein Actor. Um in Akka eine Instanz eines Actors zu erzeugen, wird eine Funktion, die den Actor erzeugen kann, in unserem Fall new Session, an die Methode actorOf des Actor-Companion-Objekts übergeben. Diese erzeugt den Actor und gibt die dazugehörige ActorRef zurück. Über die ActorRef kann der Actor anschließend gestartet werden. Die KlassenParameter von Session sind, wie wir weiter unten sehen werden, der User und die Referenz auf den MovieStore. Als Reaktion auf die Logout-Message wird die Session gestoppt und entfernt. Die beiden Messages ShowAvailable und Rent werden an die jeweilige Session weitergeleitet. Durch die Verwendung der ActorRef-Methode forward bleibt der Absender der Message unverändert. Aufgrund dessen kommt ein reply als Reaktion in der Session nicht beim SessionManagement, sondern beim ursprünglichen Absender an. Wenn der SessionManagement-Actor mit stop beendet wird, müssen die anders nicht erreichbaren Sessions auch beendet werden. Dies implementieren wir durch 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) getOrElse Error("Cannot show movies!") ) case Rent(_, serial) => self.reply( moviestore !! RentMovie(age, serial) getOrElse Error("Cannot rent movie #"+serial) ) } } Die Klasse Session ist in Listing 11.10 zu sehen. Das Alter des Users wird nur einmal berechnet, und zwar beim Erzeugen der Session. Die receive-Methode reagiert auf die beiden Messages, die vom SessionManagement an die Session geschickt werden können. Sinn und Zweck in beiden Fällen ist die Anreicherung der Anfrage um das Alter des Benutzers. Damit kann der Benutzer bei einer
11.4 Software Transactional Memory
297
Anfrage nicht „schummeln” und bekommt wirklich nur die Filme, die für ihn zugelassen sind. Die Session sendet die Anfragen an die ActorRef, die den MovieStore repräsentiert. Die dazu verwendete Methode !! wartet auf eine Antwort. Diese wird mit reply zurückgesendet. Nachdem es sich bei der Antwort um eine Option handelt, wird diese zum Weiterschicken entweder ausgepackt oder durch eine Error-Message ersetzt.
11.4
Software Transactional Memory
Mit Software Transactional Memory (STM) wird die Idee der Datenbank-Transaktion in den Hauptspeicher übertragen, um mit mehreren Threads auf dieselben Daten zugreifen zu können. Die schwierige Aufgabe, Locks zu setzen und dabei Deadlocks zu vermeiden, wird damit unnötig. Von den ACID4 -Eigenschaften einer Datenbanktransaktion bieten STM-Transaktionen die drei ersten (ACI). Die Dauerhaftigkeit 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 einfach so umbauen, dass für jeden Client über die Session hinaus auch ein eigener MovieStore-Actor gestartet wird, der die Requests des Clients unabhängig von allen anderen behandelt. Die Maps mit den verfügbaren und verliehenen Filmen sollen aber von allen gemeinsam genutzt werden. Die Herausforderung ist also, die beiden Datenstrukturen so zu verwalten, dass alle darauf zugreifen können, aber der Zustand immer konsistent ist. Das heißt zum Beispiel, beim Ausleihen muss ein Film aus available heraus- und in rent hineingenommen werden. Dies kann mit STM in einer Transaktion erfolgen, d.h. entweder war beides erfolgreich oder der Zustand vorher ist für die anderen Actors sichtbar. Was wir dazu benötigen, sind transaktionale Referenzen (Instanzen der Klasse Ref), in denen die Daten gespeichert werden. Zugriff auf solche Refs ist nur innerhalb von Transaktionen möglich. Der Wert, den eine Ref aufnimmt, sollte selbst unveränderbar sein. Die ersten Änderungen, die wir am Trait MovieStore vornehmen, ist das Umwandeln der beiden Felder available und rent in transaktionale Referenzen (siehe Listing 11.11). Listing 11.11: Die Felder available und rent des Traits MovieStore als transaktionale Referenzen
import se.scalablesolutions.akka.stm.local.Ref protected 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
298
11 Akka – Actors und Software Transactional Memory
1. Wir haben aus der var einen val gemacht. Die Ref muss immer dieselbe bleiben. 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ürlich nicht notwendig, um STM zu nutzen. Wir könnten auch hier gleich die Ref erzeugen, aber wir wollen dieselbe Ref ja auch in den anderen MovieStores verwenden. Um über die Referenzen auf den Inhalt zu kommen, müssen wir die Methode get verwenden. Den Inhalt einer Referenz verändern wir mit der Methode alter. Schließlich verwenden wir noch atomic, um einen Block zu einer Transaktion zu machen. Die im Vergleich zum bisher entwickelten MovieStore veränderten Methoden sind in Listing 11.12 dargestellt. Listing 11.12: Die Methoden des Traits MovieStore, die für die Einbindung von STM verändert werden mussten
import se.scalablesolutions.akka.stm.local.atomic def addToStore(movie: Movie) { MovieStore.serial += 1 atomic { available alter (_ + (MovieStore.serial -> movie)) } } def rentMovieAge(age: Int, serial: Int): Option[Movie] = atomic { val movieOption = available.get.get(serial) movieOption match { case None => None case Some(movie @ Movie(_,r)) => if (r movie)) movieOption } else None } } def returnMovie(serial: Int) = { atomic { val movie = rent.get.apply(serial) rent alter (_ - serial) available alter (_ + (serial -> movie)) } }
11.4 Software Transactional Memory
299
def availableMoviesForAge(age: Int) = atomic { available.get.filter{ case (_,Movie(_,r)) => r (id -> user)} toMap
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 Beispielfilme dann im MovieStore zur Verfügung. Schließlich müssen wir noch eine Implementierung für die abstrakte Methode newMovieStore aus SessionManagement angeben (siehe Listing 11.14). Listing 11.14: Die Methode newMovieStore zum Erzeugen eines neuen MovieStoreActors 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 rent werden auf die Referenz des MovieStoreServices gesetzt. Dies können wir so natürlich nicht mit this.available bzw. this.rent machen. Daher müssen wir uns noch eine Self-Type-Annotation (siehe Abschnitt 5.7.8) mit einem anderen Bezeichner definieren. Der üblicherweise verwendete Bezeichner self kann in einem Akka-Actor nicht verwendet werden, da es sich dabei um die ActorRef handelt. Wir haben daher den Bezeichner service gewählt, d.h. die Definition der Klasse beginnt mit class MovieStore ... { service => Damit in den so erzeugten MovieStores keine Filme hinzugefügt werden können, redefinieren wir addToStore mit dem konstanten Wert () vom Typ Unit. Der erzeugte Actor wird schließlich noch mit start gestartet.
302
11 Akka – Actors und Software Transactional Memory
Um den MovieStoreService als Service über das Netzwerk zur Verfügung zu stellen, 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 9999 und 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.RemoteNode RemoteNode.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 unseren MovieStoreService zu testen. Die folgende Sitzung zeigt den Start des Services 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, den wir in einer weiteren Scala-Shell nutzen können. Der Client-Code ist in Listing 11.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(
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) println("error while receiving movielist: "+msg) } } def returnM(serial: Int) = movieStore ! Return(serial) } In der Klasse MovieStoreClient wird die Verbindung zum MovieStoreService über das RemoteClient-Objekt hergestellt. Auch wenn der Client in diesem Beispiel auf dem gleichen Host läuft, könnte er so auch auf einem anderen Rechner laufen. Es müsste dann nur localhost durch den Namen des Rechners ersetzt werden, auf dem der MovieStoreService läuft. Die drei Methoden login, logout und returnM senden die jeweils notwendige Message mit ! an den MovieStoreService und warten somit nicht auf eine Antwort. Die beiden Methoden rent und show senden ihre Message mit !! und weisen das Ergebnis, die Antwort-Message, einem val zu. Mithilfe von Pattern Matching wird die Antwort ausgewertet und entsprechend mit println ausgegeben. Zum Ausprobieren starten wir in einem zweiten Terminal mit sbt console eine weitere Scala-Shell und können, wie in der folgenden Sitzung dargestellt ist, mit dem Service arbeiten:
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> login 11:33:58.819 [run-main] DEBUG s.s.akka.remote. RemoteClientHandler - [id: 0x273d6d53] OPEN 11: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 anderen UserID oder auch mit derselben anmelden. An dieser Stelle wollen wir unseren kurzen Blick auf das sehr mächtige AkkaFramework beenden, auch wenn wir viele weitere interessante Features wie beispielsweise den Akka-Microkernel, Persistenz, Lift-Integration und Fehlertoleranz im Rahmen dieses Buches nicht besprechen konnten. Die unter http: //doc.akkasource.org/ bereitgestellte Dokumentation ist sehr gut und umfangreich, sodass damit einer Vertiefung Ihrer Kenntnisse im Bereich Akka nichts mehr im Wege steht.
Schlusswort Das ist sie also: meine Einführung in Scala. Ich hoffe, Sie hatten beim Lesen genauso viel Spaß wie ich beim Ausdenken, Schreiben und Programmieren. Das, was ich Ihnen in diesem Buch gezeigt habe, bezieht sich auf die im Oktober 2010 aktuellen Versionen von Scala, den Tools und den Frameworks. Obwohl sich in der Entwicklung sehr viel bewegt, bleibt der Inhalt sicher lange nützlich und aktuell, und es kommen nur neue Features hinzu. So wird Scala z.B. mit Version 2.9 parallele Collections bekommen. Den auf der Website zu diesem Buch http://scala.obraun.net/ verlinkten Code werde ich jeweils an neue Versionen anpassen. Die Scala-Welt und das Universum der funktionalen Programmierung haben aber noch sehr viel mehr zu bieten, als ich auf den vorangegangenen Seiten präsentieren konnte. So implementiert das Scalaz-Framework5 weitere Konzepte der funktionalen Programmierung, und viele Scala-Enthusiasten wagen mindestens einen Blick in Richtung einer rein funktionalen Sprache wie etwa Haskell. Sollten Sie noch Fragen oder Anmerkungen haben, zögern Sie nicht, mir unter
[email protected] zu schreiben. Wenn Sie noch weiteren Informationsbedarf haben oder Ihren Chef oder Auftraggeber überzeugen wollen, auf Scala umzusteigen, versuche ich gerne, Ihnen dabei zu helfen. Ich würde mich freuen, von Ihnen zu hören.
5
http://code.google.com/p/scalaz/
Literaturverzeichnis [Agh86]
A GHA, Gul: ACTORS: A Model of Concurrent Computation in Distributed Systems, MIT, Diss., 1986
[Arm07]
A RMSTRONG, Joe: Programming Erlang: Software for a Concurrent World. Pragmatic Programmers, 2007
[Ass05]
A SSISI, Ramin: Eclipse 3. Carl Hanser Verlag, 2005
[Bir98]
B IRD, Richard: Introduction to Functional Programming using Haskell. Prentice Hall, 1998
[BOSW98] B RACHA, Gilad; O DERSKY, Martin; S TOUTAMIRE, David; WADLER, Philip: GJ Specification. 1998 [BS07]
B EUST, Cedric; S ULEIMAN, Hani: Next Generation Java Testing: TestNG and Advanced Concepts. Addison-Wesley Longman, 2007
[Bö08]
B ÖCK, Heiko: NetBeans Platform 6 - Rich-Client-Entwicklung mit Java. Galileo Computing, 2008
[CBWD09] C HEN -B ECKER, Derek; W EIR, Tyler; D ANCIU, Marius: The Definitive Guide to Lift: A Scala-based Web Framework. Apress, 2009 [CGLO06] C REMET, Vincent; G ARILLOT, François; L ENGLET, Sergueï; O DERSKY, Martin: A Core Calculus for Scala Type Checking. In: Proceedings of MFCS 06, Stará Lesná (2006) [Cli81]
C LINGER, William: Foundations of Actor Semantic, MIT, Diss., 1981
[Hew09]
H EWITT, Carl: ActorScript(TM): Industrial strength integration of local and nonlocal concurrency for Client-cloud Computing. (2009)
[HMUL05] H AROLD, Elliotte R.; M EANS, W. S.; U DEMADU, Katharina; L ICHTEN BERG , Kathrin: XML in a Nutshell. O’Reilly, 2005 [Hug89]
H UGHES, John: Why Functional Programming Matters. In: The Computer Journal (1989)
308
Literaturverzeichnis
[Hut92]
H UTTON, Graham: Higher-order functions for parsing. In: Journal of Functional Programming (1992)
[Mey00]
M EYER, Bertrand: Object-Oriented Software Construction. Prentice Hall, 2000
[MH03]
M ONIN, Jean F.; H INCHEY, Michael G.: Understanding formal methods. Springer, 2003
[NW06]
N AFTALIN, Maurice; WADLER, Philip: Java Generics and Collections. O’Reilly, 2006
[Ode00]
O DERSKY, Martin: Programming With Functional Nets / École Polytechnique Fédérale de Lausanne. 2000. – Forschungsbericht
[Ode10a]
O DERSKY, Martin: A Postfunctional Language. http://www.scala-lang.org/node/4960, Januar 2010
[Ode10b]
O DERSKY, Martin: The Scala Language Specification. http://www.scala-lang.org/, 2010
[Oka99]
O KASAKI, Chris: Purely Functional Data Structures. Cambridge University Press, 1999
[OSG08]
O’S ULLIVAN, Bryan; S TEWART, Donald B.; G OERZEN, John: World Haskell. O’Reilly, 2008
[OSV08]
O DERSKY, Martin; S POON, Lex; V ENNERS, Bill: Programming in Scala: A comprehensive step-by-step guide. Artima, 2008
[OW97]
O DERSKY, Martin; WADLER, Philip: Pizza into Java: Translating theory into practice. In: Proc. 24th ACM Symposium on Principles of Programming Languages, Paris, France (1997)
[PH06]
P EPPER, Peter; H OFSTEDT, Petra: Funktionale Programmierung: Sprachdesign und Programmiertechnik. Springer, 2006
[Pie02]
P IERCE, Benjamin: Types and Programming Languages. MIT Press, 2002
[Son08]
S ONATYPE: Maven: The Definitive Guide. O’Reilly, 2008
[ST95]
S HAVIT, Nir; T OUITOU, Dan: Software Transactional Memory. In: Proceedings of the 14th ACM Symposium on Principles of Distributed Computing (1995)
[Tan09]
TANENBAUM, Andrew S.: Moderne Betriebssysteme. Pearson Studium, 2009
Real
Literaturverzeichnis
[TJJV04]
309
T RUYEN, Eddy; J OOSEN, Wouter; J ØRGENSEN, Bo N.; V ERBAETEN, Pierre: A Generalization and Solution to the Common Ancestor Dilemma Problem in Delegation-Based Object Systems. In: Proceedings of the 2004 Dynamic Aspects Workshop (2004)
[TLMG10] TAHCHIEV, Petar; L EME, Felipe; M ASSOL, Vincent; G REGORY, Gary: JUnit in Action. Manning, 2010 [WBB10]
W EBER, Bernd; B AUMGARTNER, Patrick; B RAUN, Oliver: OSGi für Praktiker. Carl Hanser Verlag, 2010
Stichwortverzeichnis Actor ! 197 Act 196 DaemonActor 207 Reactor 208 RemoteActor 211 ReplyReactor 208 TIMEOUT 198 actor 194 alive 211 loopWhile 205 loop 205 reactWithin 203 react 203 receiveWithin 198 receive 197 register 211 remoteActor 212 self 197 Scheduler 209 Annotation 44 deprecated 44 serializable 44 spezialized 157 tailrec 107 throws 128 transient 44 unchecked 44, 124 volatile 44 Self-Type 160 Varianz-Annotation 150
Argument Default 36 Named 36 Array 29 args 40 Bedarfsauswertung 102 Bezeichner 31 Call-by-Name 140 Call-by-Value 139 Closure 104 Collections 170 Context Bound 155 Currysierung 132 dynamisches Binden 72 Eclipse 22 Enumeration 65 Evaluation Eager 102 Lazy 102 Exception Handling 126 Extraktor 128 Feld 47 Filter 142 For-Comprehension 141 For-Expression 141 Funktion 35, 104 dropWhile 112 exists 112
312
filterNot 112 filter 112 flatMap 144 foldLeft 112 foldRight 112 forall 112 foreach 111 map 110 parseAll 183 partition 112 reduceLeft 113 reduceRight 113 scanLeft 113 scanRight 113 span 112 takeWhile 112 Angabe des Ergebnistyps notwendig 36 Closure 104 endrekursiv 106 höherer Ordnung 108 ineinander verschachtelte 37 Literal 104 Parameterliste 35 partiell angewendete 110 partielle 124 rekursive 104 tail recursiv 106 variable Parameteranzahl 37 Gültigkeitsbereich 41 Generator 41, 141 Higher-Order-Function 108 IDE IntelliJ IDEA 24 NetBeans 23 Scala-IDE for Eclipse 22 Implicits 95 Import Selector Clause 81 IntelliJ IDEA 24 Interface Rich 83
Stichwortverzeichnis
Java-Integration 29 Klasse -Parameter 62 AnyRef 167 AnyVal 167 Any 165 Array 29, 155 ArrowAssoc 98, 169 Future 207 ListBuffer 30 List 28, 111 NodeSeq 178 ScalaObject 167 Set 34 TailCalls 107 abtrakte 73 Basis- 67 Case 119 leerer Rumpf 70 Sub- 67 Super- 67 versiegelte 122 Kommando fsc 10, 15 javap 15 mvn 17 sbaz 10 sbt 19 scalac 10, 13 scaladoc 10, 16 scalap 10, 15 scala 9, 11 Kommentar 33 Konstruktor 62 auxiliary 63 primärer 62 zusätzlicher 63 Kontrollstruktur do-while 40 for 41 if-else 40 while 40 eigene 135
Stichwortverzeichnis
Manifest 156 Maven-Scala-Plugin 17 Member 47 abtraktes 73 Methode 47 Überladen 55 != 166 _= 50 += 30 -> 169 == 166 andThen 125 apply 28, 59 asInstanceOf 167 assert 169 assume 169 copy 121 curried 133 equals 166 eq 168 error 169 exit 169 foreach 172 hashCode 166 isDefinedAt 125 isInstanceOf 167 iterator 172 lift 125 main 43 ne 168 orElse 125 println 170 readLine 170 require 169 r 131 toString 166 unapply 120 unary_ 59 update 29, 59 empty-paren 58 Extraktor- 120 Factory- 64 Getter 50 parameterless 58
313
Redefinition 50 Setter 50 Mixin 84 Nebenläufigkeit 193 NetBeans 23 Objekt Actor 194 Predef 169 Companion- 53 Operator