Arbeitsgruppe für Programmiersprachen und Übersetzerkonstruktion Institut für Informatik Christian-Albrechts-Universität zu Kiel Seminararbeit Die multiparadigmatische Programmiersprache Scala Lasse Kristopher Meyer Wintersemester 2012/2013 Betreut von Björn Peemöller ii Inhaltsverzeichnis Inhaltsverzeichnis Inhaltsverzeichnis ii Abbildungsverzeichnis iii Tabellenverzeichnis iii Listings iii 1 Einordnung 1 1.1 Grundlegende Eigenschaften . . . . . . . . . . . . . . . 1.1.1 Scala als objektorientierte Programmiersprache 1.1.2 Scala als funktionale Programmiersprache . . . 1.2 Geschichte und Entwicklung . . . . . . . . . . . . . . . 1.3 Einflüsse anderer Programmiersprachen . . . . . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 2 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 6 6 6 Anwendungsgebiete 5 2.1 Wofür ist Scala beabsichtigt? . . . 2.2 Wo wird Scala bereits eingesetzt? . 2.2.1 Beispiel 1: Twitter . . . . . 2.2.2 Beispiel 2: Play Framework 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausgewählte Konzepte 7 3.1 Allgemeine Konzepte . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Variablen und Funktionen . . . . . . . . . . . . . . . 3.1.2 for-Ausdrücke . . . . . . . . . . . . . . . . . . . . . 3.2 Objektorientierte Konzepte . . . . . . . . . . . . . . . . . . 3.2.1 Grundlagen, Klassen und Vererbung . . . . . . . . . 3.2.2 Singleton-Objekte . . . . . . . . . . . . . . . . . . . 3.2.3 Traits . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Funktionale Konzepte . . . . . . . . . . . . . . . . . . . . . 3.3.1 Grundlagen, Funktionen und Closures . . . . . . . . 3.3.2 Case Classes, generische Typen und Pattern Matching 3.3.3 Aktoren und Nebenläufigkeit . . . . . . . . . . . . . 4 5 . . . . . . . . . . . 7 7 8 9 9 11 12 13 13 15 16 Technische Unterstützung 18 4.1 Integration mit Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Compiler, Interpreter, IDEs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Der Typesafe Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 18 19 Diskussion und Zusammenfassung 20 Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Lasse Kristopher Meyer iii Abbildungsverzeichnis/Tabellenverzeichnis/Listings Abbildungsverzeichnis 4.1 Der Scala-Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Tabellenverzeichnis 1.1 Auszug aus der Versionsgeschichte Scalas . . . . . . . . . . . . . . . . . . . . . . 1.2 Einflüsse anderer Programmiersprachen auf den Entwurf Scalas . . . . . . . . . . 3 4 Listings 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 Filtern mit for-Ausdrücken . . . . . . . . . . . Rückgabewerte von for-Ausdrücken . . . . . . Complex.scala (1. Teil) . . . . . . . . . . . . . Abstrakte Klassen und Vererbung . . . . . . . . Complex.scala (2. Teil) . . . . . . . . . . . . . Singleton-Objekte und Vererbung . . . . . . . . Abstrakte und konkrete Elemente in Traits . . . Stapelbare Modifikationen . . . . . . . . . . . . Closures . . . . . . . . . . . . . . . . . . . . . . Endrekursive Funktionen . . . . . . . . . . . . . Tree.scala (1. Teil) . . . . . . . . . . . . . . . Tree.scala (2. Teil) . . . . . . . . . . . . . . . Aktoren implementieren (Das Actor-Trait) . . . Aktoren implementieren (Die actor-Methode) Lasse Kristopher Meyer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 8 9 10 11 11 12 12 14 15 15 16 16 17 1 1 Einordnung 1 Einordnung Ziel des ersten Kapitels ist die Vermittlung grundlegender Eigenschaften der Programmiersprache Scala und deren Einordnung. Zu diesem Zweck soll die Bedeutung der Programmierparadigmen Scalas hervorgehoben werden. Die zwei letzten Abschnitte erläutern anschließend die Hintergründe der Entwicklungsgeschichte und die Einflüsse anderer Sprachen auf den Sprachentwurf. 1.1 Grundlegende Eigenschaften Scala ist eine objektorientierte (imperative), funktionale und statisch getypte Programmiersprache. Der Name Scala ist ein Kofferwort und steht für „scalable language“. Die Wahl dieses Namens beruht auf dem Gedanken Scala den Ansprüchen und Anforderungen des Nutzers anpassen zu können. Dabei kann Scala einerseits als Skriptsprache verwendet werden, andererseits bietet es aber auch Programmierkonstrukte ähnlich denen in Java, die die Entwicklung großer Projekte unterstützen sollen. Zu diesem Zweck ist der Sprachkern Scalas vergleichsweise klein gehalten. Vielmehr kann Scala manuell um die notwendigen Funktionen erweitert werden. Dazu stehen dem Nutzer unter anderem bekannte objektorientierte Konzepte wie Vererbung zur Verfügung, gleichzeitig bietet Scala aber auch die Möglichkeit funktionale Elemente wie Funktionen höherer Ordnung zu verwenden, um beispielsweise eigene Kontrollstrukturen zu entwerfen. Laut den Entwicklern selbst resultiert die Skalierbarkeit der Programmiersprache Scala aber vor allem aus der Kombination objektorientieter und funktionaler Programmierung [OSV10], weshalb diese beiden Aspekte im Folgenden näher beleuchtet werden. 1.1.1 Scala als objektorientierte Programmiersprache Scala ist, ähnlich wie die Programmiersprache Smalltalk, rein objektorientiert. Das bedeutet, dass in Scala ausnahmslos jeder Wert ein Objekt ist und jede Operation ein Methodenaufruf. So ist die Addition 4 + 2 beispielsweise als ein Aufruf der Methode + des Objekts 4 vom Typ Int mit dem Parameter 2 zu verstehen.1 Im Gegensatz zu Java kennt Scala keinen Unterschied zwischen primitiven Datentypen und anderen Objekten, wenngleich Scalas Werttypen bei der Umwandlung in Java-Bytecode auf Javas primitive Datentypen zurückgeführt werden. Neben bekannten Konzepten des objektorientierten Programmierens unterstützt Scala auch neuartigere objektorientierte Programmiertechniken wie die Verwendung von Traits (s. Abschnitt 3.2.3). 1 In Scala können Methoden in Operatorenschreibweise definiert werden. Eine äquivalente Schreibweise ist (4).+(2). Lasse Kristopher Meyer 2 1 Einordnung 1.1.2 Scala als funktionale Programmiersprache Da Scala ebenso funktional ist und als logische Konsequenz aus der Tatsache, dass in Scala jeder Wert ein Objekt ist, sind auch Funktionen Objekte. Als First-Class-Objekte können Funktionen Parameter oder Rückgabewerte sein, sie können wie andere Werte als Literale notiert werden und in Variablen gespeichert oder innerhalb anderer Funktionen definiert werden. Diese Eigenschaften ermöglichen beispielsweise die Definition eigener Kontrollstrukturen, was in den Standard-Bibliotheken Scalas auch konsequente Verwendung findet (z. B. bei der Implementierung des Actor Models, s. Abschnitt 3.3.3). Neben der Handhabung von Funktionen finden sich in Scala auch viele weitere Besonderheiten funktionaler Sprachen wie List Comprehensions, Lazy Initialization, Pattern Matching, Tail Call Optimization oder Currying. Außerdem bietet Scala eine breite Unterstützung für die Generierung von seiteneffektfreiem Code und die Wahrung referenzieller Transparenz. Dazu enthält die Standard-Bibliothek beispielsweise neben den veränderbaren (mutable) eine große Anzahl an unveränderbaren (immutable) Datenstrukturen (wie z. B. die Datentypen List, Set und Map). Letztlich ist es dem Nutzer in den meisten Fällen selbst überlassen mit welchem Paradigma er ein Problem lösen will. Grundsätzlich können Scala-Programme wie Java-Programme auch vollkommen imperativ aufgebaut werden (bis auf syntaktische Unterschiede und wenige Änderungen an objektorientierten Konzepten kann in Scala genauso wie in Java programmiert werden), zusätzlich bietet sich aber auch häufig die Möglichkeit imperative Konstrukte zu vermeiden und funktionale Lösungsansätze einzubringen oder beide Programmierparadigmen zu kombinieren. 1.2 Geschichte und Entwicklung Die Programmiersprache Scala wird seit 2001 am Labor für Programmiermethoden an der École polytechnique fédérale de Lausanne (EPFL), einer technisch-naturwissenschaftlichen Universität in der Schweiz, von einer Gruppe von Wissenschaftlern unter der Leitung von Martin Odersky entwickelt. Neben dem eigentlichen Sprachentwurf gehört dabei auch die Entwicklung der zugehörigen Bibliotheken und des Compilers scalac zu den Aufgaben des Teams. Für den Erfinder der Programmiersprache, Martin Odersky, war die Entwicklung von Scala nicht der erste Versuch funktionale und objektorientierte Programmieraspekte zu verknüpfen. Nachdem er um 1988 während des Abschlusses seiner Promotion seine Begeisterung für die funktionale Programmierung entdeckte und bald darauf auf die objektorientierte Programmiersprache Java (damals noch in der Alpha-Version) aufmerksam wurde, entschied Martin Odersky sich, mit seinem damaligen Forschungspartner eine funktionale Sprache auf Basis der Java Virtual Machine zu schreiben. Daraus entstand die Programmiersprache Pizza, welche eine Obermenge von Java darstellte und drei Aspekte der funktionalen Programmierung realisierte: Generische Programmierung, Funktionen höherer Ordnung und Pattern Matching. Pizza wurde 1996 veröffentlicht, war aber selbst nach Oderskys Ansicht mäßig erfolgreich darin, die Implementierung einer funktionalen Sprache in der Java-Laufzeitumgebung zu demonstrieren [OV09]. Aus den Arbeiten an der generischen Programmierung und in Kollaboration mit Sun Microsystems entstand um 1997 GJ (Generic Java), welches sechs Jahre später mit Java 5 offiziell in den Sprachumfang eingebunden wurde. Zuvor wurde mit der Veröffentlichung von Java 3 bereits der von Odersky geschriebene GJ-Compiler als neuer Standard-Compiler javac übernommen. Lasse Kristopher Meyer 3 1 Einordnung Nachdem Odersky 1999 eine Professur an der EPFL antrat sah er die Gelegenheit seine neugewonnene akademische Freiheit zu nutzen, um sich der Entwicklung einer neuen Programmiersprache zu widmen, ohne sich den Einschränkungen Javas unterwerfen zu müssen [Ode06]. War es vorher noch Ziel seiner Arbeit gewesen die Sprache Java zu verbessern und zu erweitern, so wollte Odersky nun eine bessere Sprache entwerfen, die sich nur noch der Infrastruktur Javas (JVM und Bibliotheken) bediente, aber nicht mehr auf der Sprache selbst basierte. Aus dieser Idee enstand Funnel, eine Programmiersprache mit minimalem Kern, die auf einem Prozesskalkül für Programmiersprachen beruhte, aber weiterhin funktionale mit objektorienterten Aspekten verknüpfte. Funnel erwies sich allerdings als zu akademisch und unpraktikabel für den allgemeinen Gebrauch, nicht zuletzt wegen der geringen Unterstützung durch Standard-Bibliotheken. Infolgedessen konzentrierten sich Odersky und seine Mitarbeiter darauf, eine multiparadigmatische Programmiersprache zu entwerfen, die einen Mittelweg zwischen dem minimalistischen Funnel und dem eher pragmatischen GJ einschlagen sollte und als vollwertige Sprache für reale Anwendungen konzipiert war. 2002 verlieh man dieser Sprache den Namen Scala. 2003 wurde Scala in der Version 1.0 für die Java-Plattform veröffentlicht, 2004 folgte eine Implementierung für .NET. Seitdem wird Scala kontinuierlich weiterentwickelt. Seit Anfang 2011 ist dieser Zustand durch eine finanzielle Förderung von 2,3 Millionen Euro über fünf Jahre vom European Research Council langfristig gewährleistet. Durch die Förderung soll vor allem die Forschung im Bereich der parallelen Programmierung vorangetrieben werden. Im Mai 2011 gründete Martin Odersky mit seinen Mitarbeitern zudem das Unternehmen Typesafe, Inc., welches sich dem kommerziellen Support Scalas Tabelle 1.1: Versionen und verwandter Softwarekomponenten wie zum Beispiel dem Play von Scala Framework (s. Abschnitt 2.2.2) widmet. Abbildung 1.2 zeigt einen kleinen Auszug aus der Versionsgeschichte von Scala. Bei vielen Versionsveröffentlichungen ergab sich die Problematik, dass der Sprachumfang wegen grundlegender Änderungen nicht mehr rückwärtskompatibel war. Deshalb waren viele Nutzer der Sprache gezwungen Teile ihres Codes umzuschreiben. Seit Version 2.0 ist auch der Compiler scalac selbst komplett in Scala geschrieben. Derzeit befindet sich Scala in der am 04.01.2013 veröffentlichten Version 2.10.0. Datum 08.12.2003 12.03.2006 11.10.2006 21.03.2007 11.09.2007 14.07.2010 04.01.2013 Version 1.0.0 2.0.0 2.2.0 2.4.0 2.6.0 2.8.0 2.10.0 1.3 Einflüsse anderer Programmiersprachen Die Entwicklung Scalas wurde durch viele andere Programmiersprachen nachhaltig beeinflusst. Tatsächlich sind viele der Funktionen. die Scala bietet, schon aus anderen Programmiersprachen bekannt, wenngleich Scala auch neue Ansätze für bekannte Konzepte anbietet (z. B. die flexible Handhabung von Traits). Tabelle 1.2 enthält eine kleine Übersicht über die Einflüsse anderer Programmiersprachen auf einige Sprachbestandteile in Scala. Einige der hier aufgeführten Bestandteile werden zudem mit Beispielen in Kapitel 3 näher beschrieben. Lasse Kristopher Meyer 4 1 Einordnung Bestandteil Syntax Einflüsse Java, C# Funktionale und objektorient. Prog. Klassenbibliotheken, Basistypen Uniform object model Ruby, Smalltalk, Python Universal nesting Uniform access principle Algol, Simula Funktionale Programmierkonzepte Actor model ML, OCaml, Haskell Java Smalltalk Eiffel Erlang Beschreibung Die Syntax der Ausdrücke, Zuweisungen, Klassen, Pakete und Imports enspricht im Wesentlichen der Java-Syntax. Verknüpfung funktionaler und objektorientierter Programmierung. Scala ist vollständig kompatibel zu Javas Bibliotheken und implementiert viele selbst. Beispielsweise werden Javas Basistypen durch eigene Klassen realisiert. In Scala werden alle Daten als Objekte betrachtet. Im Gegensatz zu Java gibt es z. B. keinen Unterschied zwischen Objekten und primitiven Datentypen. Fast alle Konstrukte in Scala können beliebig tief geschachtelt werden (Klassen in Klassen, Funktionen in Funktionen, ...). Der Zugriff auf Bauteile einer Klasse (Attribute, Methoden) kann einheitlich erfolgen wenn keine Parameter übergeben werden müssen. So bleibt verborgen ob der Inhalt einer Variable ausgelesen oder eine Methode aufgerufen wird. Typische Funktionen höherer Ordnung sind bspw. in Scalas Standard-Bibliothek enthalten (z. B. die Methoden map, foldl und foldr). Programmiermodell für Nebenläufigkeit. Aktoren sind nebenläufige Einheiten mit getrenntem Speicherbereich, die Nachrichten an andere Aktoren versenden, Nachrichten erhalten und diese verarbeiten können. Tabelle 1.2: Einflüsse anderer Programmiersprachen auf den Entwurf Scalas Diese Auflistung entält nur eine Teilmenge der Bestandteile, die in Scala integriert wurden. Die genannten Sprachen gelten allerdings als die wichtigsten Einflüsse auf den Sprachentwurf [OSV10]. Lasse Kristopher Meyer 5 2 Anwendungsgebiete 2 Anwendungsgebiete Im zweiten Abschnitt soll vorerst erläutert werden mit welcher Intention die Programmiersprache Scala entwickelt wurde, welche Einsatzgebiete beabsichtigt waren und welche Eigenschaften zu diesem Zweck in Scala integriert wurden. Anschließend wird an zwei Beispielen verdeutlicht, wo Scala bereits eingesetzt wird. 2.1 Wofür ist Scala beabsichtigt? Auf der Internetpräsenz von Typesafe wird Scala als eine „general purpose programming language“ [Typ] bezeichnet deren Entwicklung folgende Ziele hatte: • Die Umsetzung bekannter Programmiermuster soll in prägnanter, eleganter Form möglich sein. Üblicherweise wird erwartet, dass Scala die Menge an Code (z. B. gegenüber Java) halbiert, vor allem aufgrund des hohen Abstraktionsniveaus funktionaler Programmierung, die dazu tendiert wesentlich weniger Code zu benötigen. • Typsicherheit durch statische Typisierung mit dem Ziel viele Arten von Fehlern bereits zur Kompilierzeit zu erkennen. Gleichzeitig unterstützt Scala Typinferenz um redundante Typangaben innerhalb des Codes auf ein Minimum zu reduzieren (s. Abschnitt 3.1.1). • Verbindung objektorientierter und funktionaler Programmierung. • Vollständige Interoperabilität mit Java und mit Javas Infrastruktur (s. Abschnitt 4.1), vor allem damit Scala ohne Schwierigkeiten in schon bestehende Java-Projekte eingebunden werden kann. • Unkomplizierte Unterstützung nebenläufiger Programmierung und Nutzung der Möglichkeiten moderner Mehrkern-Architekturen, insbesondere durch die Eigenschaften funktionaler Programmierung. Konstante Werte, referentielle Transparenz und eigene Speicherbereiche für nebenläufige Einheiten sollen den Code vorhersehbarer und somit verlässlicher machen. • Skalierbarkeit. Scala ist ebenso für kleine Anwendungen wie auch für kommerzielle Projekte mit großen Entwicklerteams konzipiert. Zur Unterstützung kommerzieller Projekte stellt Typesafe beispielsweise die Software-Plattform Typesafe Stack zur Verfügung (s. Abschnitt 4.3). • Erweiterbarkeit der Sprache. Durch eigene Bibliotheken hinzugefügte Sprachkomponenten sollen sich dabei wie native Sprachunterstützung anfühlen. Dadurch soll es möglich sein, domänenspezifische Sprachen (DSL) zu erstellen, damit Scala auch in Bereichen eingesetzt werden kann, in denen keine Programmierer mit umfangreichem Zusatzwissen vorhanden sind. Lasse Kristopher Meyer 6 2 Anwendungsgebiete 2.2 Wo wird Scala bereits eingesetzt? Trotz der eher langsam (aber kontinuierlich) zunehmenden Popularität der Sprache unter Entwicklern [Red12] setzen bereits viele Unternehmen bei der Entwicklung ihrer kommerziellen Software und Produktionssysteme auf Scala. Zu Typesafes Kunden zählen beispielsweise LinkedIn, Twitter, The Guardian, Sony und Siemens [Typ12]. Die Einsatzgebiete Scalas reichen dabei von der Programmierung asynchroner, nebenläufiger Softwarekomponenten bis hin zur Entwicklung komplexer Bibliotheken zur Aktualisierung und Verwaltung von Datenbankschemata. Weiterhin ist Scala Bestandteil einiger Web Application Frameworks wie dem Play Framework und dem Lift Framework. Anhand der Beispiele Twitter und dem Play Framework soll im Folgenden erläutert werden, wo und warum Scala in kommerziellen Anwendungen oder Open-SourceProjekten bereits Verwendung findet. 2.2.1 Beispiel 1: Twitter Der Mikrobloggingdienst Twitter war ursprünglich eine reine Ruby on Rails-Applikation. Heute spielt Rails vor allem im Front-End-Bereich die größte Rolle, während im Back-End-Bereich seit 2008 zunehmend Programmiersprachen verwendet werden, die zur Java Virtual Machine kompatibel sind. Scala kommt seitdem primär bei der Bereitstellung langlebiger Serverprozesse auf der JVM zum Einsatz. Weiterhin wurde das in Ruby geschriebene Message-Queueing-System, welches beispielsweise Nutzeraktionen in Warteschlangen einreiht und an Back-End-Daemons zur Bearbeitung weiterleitet, komplett in Scala umgeschrieben. Als Gründe für den Umstieg von Ruby auf Scala nennen die Entwickler einerseits die Flexibilität des Typsystems, das im Gegensatz zu Rubys Typsystem zwar statisch ist, durch Typinferenz aber nicht immer strenge Typangaben verlangt, andererseits ermöglichte Scala auch einen besseren Zugang zur Umsetzung asynchroner Prozesse aufgrund der Kompatibilität zur JVM [JPPV09]. 2.2.2 Beispiel 2: Play Framework Das Play Framework ist ein in Java und Scala geschriebenes, quelloffenes Web Application Framework und Teil des Typesafe Stacks. Seit Version 1.1 unterstützt das Play Framework neben Java die Programmiersprache Scala und seit Version 2.0 (die zusammen mit dem Typesafe Stack 2.0 veröffentlicht wurde) ist das Framework selbst komplett in Scala geschrieben. Der Umstieg auf Scala war laut den Entwicklern die konsequente Anpassung an die steigenden Ansprüche an Webanwendungen. Scalas Actor Model (bzw. dessen Implementierung mit der Middleware Akka, ebenfalls Bestandteil des Typesafe Stacks) bildet beispielsweise die Grundlage für die Verwaltung nebenläufiger, persistenter Serververbindungen und die Entwicklung verteilter Systeme. Das ursprünglich Groovy-basierte und somit dynamisch getypte Template-System des Play Frameworks wurde zugunsten der frühzeitigen Fehlererkennung und Fehlerbehandlung durch statische Typisierung ebenfalls mit Scala umgesetzt [Pla11]. Lasse Kristopher Meyer 7 3 Ausgewählte Konzepte 3 Ausgewählte Konzepte In Kapitel 3 werden anhand von Beispielen zu den wichtigsten allgemeinen, objektorientierten und funktionalen Konzepten die Syntax und die Struktur von Scala erläutert. Scalas Syntax ist stark durch Syntaxkonventionen aus Java und C# beeinflusst. Im Folgenden werden nur die wichtigsten Abweichungen und Ergänzungen behandelt. Grundsätzlich wird in Kapitel 3 Basiswissen über Syntax und Strukturen der Programmiersprache Java vorausgesetzt. 3.1 Allgemeine Konzepte 3.1.1 Variablen und Funktionen Scala unterscheidet zwei Arten von Variablen, vals (values) und vars (variables). Dabei entsprechen vals Javas final-Variablen, es kann ihnen also nur ein einziges Mal ein Wert zugewiesen werden. vars hingegen kann jederzeit ein neuer Wert zugewiesen werden. Folgendes Beispiel demonstriert die Definition zweier Variablen: val n: Int = 42 var s: String = "Hello, world!" Das Beispiel zeigt zwei Unterschiede zu Java: Erstens erfolgt die Typangabe einer Variable in Scala nach dem Schema Variable: Typ (statt Typ Variable) und zweitens müssen Statements in Scala nicht zwingend mit einem Semikolon abgeschlossen werden. In den meisten Fällen behandelt der Compiler stattdessen ein Zeilenende wie ein Semikolon (Semicolon Inference). Die Unterscheidung in vals und vars resultiert aus der Verknüpfung objektorientierter (imperativer) und funktionaler Programmierung. vals sind dabei häufig eher im funktionalen Kontext als Bindung eines Namens an einen Ausdruck zu verstehen (vgl. define in Racket). Die Definition einer Funktion wird in Scala mit dem Schlüsselwort def eingeleitet: def greet(): Unit = { println("Hello, world!") } def add(x: Int, y: Int): Int = { x + y } Der Rückgabetyp Unit der Funktion greet ist vergleichbar mit Javas void-Typ und kennzeichnet eine Funktion, die nur ihrer Seiteneffekte wegen aufgerufen wird. Per Konvention werden Rückgabetyp und das Gleichheitszeichen bei solchen Funktionen weggelassen, um das Aussehen einer Prozedur zu erreichen. Die Funktion add hingegen liefert ein Ergebnis vom Typ Int zurück. Enthält der Rumpf einer Funktion mit einem Rückgabewert1 , der nicht vom Typ Unit ist, nur ein einziges Statement, so sind die geschweiften Klammern optional. Die beiden Funktionen ließen sich ein wenig prägnanter also auch wie folgt definieren: def greet() { println("Hello, world!") } def add(x: Int, y: Int): Int = x + y 1 Ohne die Angabe des Schlüsselworts return ist der Wert des letzten Ausdrucks im Rumpf der Rückgabewert. Lasse Kristopher Meyer 8 3 Ausgewählte Konzepte Sowohl für Variablen als auch für Funktionen in Scala gilt: Redundante (doppelte) oder herleitbare Typangaben können oft vermieden werden: val b = true var l = List("a", "b") def square(x: Double) = x * x // instead of ’val b: Boolean = true’ // instead of ’var l: List[String] = List("a", "b")’ // instead of ’def square(x: Double): Double = x * x’ In diesen Fällen kann der Compiler aus dem Kontext rückschließen, welche Typangaben er beim Übersetzen ergänzen muss. Dieser Mechanismus wird als Typinferenz bezeichnet und sorgt z. B. dafür, dass das Programmieren in Scala deutlich weniger Schreibarbeit erfordert als in Java. 3.1.2 for-Ausdrücke Die meisten Kontrollstrukturen (if-Abfragen, while-Schleifen, ...) in Scala sind syntaktisch und semantisch ähnlich zu benutzen wie in Java. Eine Ausnahme bilden for-Ausdrücke, welche im Gegensatz zu anderen imperativen Programmiersprachen die Iteration über mehrere Collections, das Filtern von Werten und das Produzieren neuer Collections erlauben. Listing 3.1 zeigt ein Scala-Programm, das alle Kommandozeilenparameter mit mehr als zwei Zeichen in die Standardausgabe schreibt. 1 2 3 4 5 6 7 8 object ForExpressions { def main(args: Array[String]) { for ( arg <- args // generator if (arg.length > 2) // filter ) println(arg) // body (single statement, curly braces optional) } } Listing 3.1: Filtern mit for-Ausdrücken Die Iteration in for-Ausdrücken erfolgt immer über mindestens einen Generator „<-“ (Zeile 4). Dieser bindet den zu betrachtenden Wert an die val-Variable arg. Filter werden über beliebig viele if-Klauseln realisiert, die sich dem Generator anschließen (Zeile 5). Wie der Name schon andeutet können for-Ausdrücke auch Werte zurückliefern. In Listing 3.2 werden alle Dateien im aktuellen Arbeitsverzeichnis in einem Array abgelegt. Danach werden alle Scala-Dateien mit einem for-Ausdruck in einem neuen Array zusammengefasst. 1 2 3 4 5 6 7 val files = (new java.io.File(".")).listFiles val scalaFiles = for ( file <- files if file.getName.endsWith(".scala") ) yield file // yield goes before the entire body Listing 3.2: Rückgabewerte von for-Ausdrücken Das Schlüsselwort yield legt in jeder Iteration den Wert des Rumpfes (hier nur file) in einer neuen Collection ab. Der Typ der neuen Collection hängt einerseits vom Typ der Collection ab über die iteriert wird und andererseits vom Typ des Wertes aus dem Rumpf. In diesem Fall haben sowohl files als auch scalaFiles den Typ Array[java.io.File]2 . Durch das Hinzufügen weiterer Generatoren im Kopf des for-Ausdrucks lassen sich im Übrigen 2 Java-Klassen lassen sich auf gewohnte Weise in Scala-Programmen verwenden (s. Abschnitt 4.1). Lasse Kristopher Meyer 9 3 Ausgewählte Konzepte geschachtelte Iterationen erreichen. Außerdem dürfen zwischen den Generatoren und Filtern Variablen definiert werden, auf die man auch noch im Rumpf des for-Ausdrucks zugreifen kann. 3.2 Objektorientierte Konzepte 3.2.1 Grundlagen, Klassen und Vererbung Üblicherweise sind Scala-Programme in Bibliotheken strukturiert, die wiederum in Pakete unterteilt sind, welche Klassen, Singleton-Objekte und Traits enthalten. Wie in Java können Komponenten einer solchen Bibliothek über das Schlüsselwort import in den eigenen Quelltext eingebunden und benutzt werden. Im Gegensatz zu Java können allerdings mehrere dieser Komponenten in einer Datei mit frei wählbarem Namen (und der Endung „.scala“) definiert werden. Klassendefinitionen werden mit dem Schlüsselwort class eingeleitet. Jede Klasse hat genau einen Primärkonstruktor, dessen Argumente (auch Klassenparameter genannt) bei der Definition direkt nach dem Klassennamen angegeben werden: class Complex(val real: Double, val imag: Double) Wird kein expliziter Zugriffmodifikator angegeben, so sind Klassen, Attribute und Methoden in Scala implizit public. Andernfalls sind nur die Modifikatoren protected3 und private erlaubt. Kennzeichnet man außerdem die Klassenparameter mit dem Variablentyp (und Modifikatoren), werden vom Compiler automatisch Attribute mit den entsprechenden Zugriffsrechten angelegt. Um mit den Objekten der Klasse Complex nun auch rechnen zu können, ergänzen wir den bisher leeren Rumpf (leere geschweifte Klammern sind optional) um ein Attribut und einige Methoden: 1 2 3 4 5 6 7 8 9 10 11 12 13 import scala.math._ class Complex(val real: Double, val imag: Double) { println("Created: " + real + " + " + imag + "i") val abs = sqrt((real * real) + (imag * imag)) def +(d: Double) = new Complex(this.real + d, this.imag) def +(that: Complex) = new Complex(this.real + that.real, this.imag + that.imag) override def toString = real + " + " + imag + "i" } Listing 3.3: Complex.scala (1. Teil) Der Scala-Compiler interpretiert jeden Code, der nicht innerhalb einer Attributs- oder Methodendefinition steht, als Bestandteil des Primärkonstruktors. Der Code in Zeile 4 wird also bei jeder Instanziierung ausgeführt. Dies zeigt auch die Ausgabe des Interpreters (s. Abschnitt 4.2): scala> val z = new Complex(1.0, 1.0) Created: 1.0 + 1.0i z: Complex = 1.0 + 1.0i Auf die Definition von Zugriffsfunktionen (Getter/Setter) für die Attribute real, imag und abs in Zeile 6 kann verzichtet werden, da durch die Deklaration als (public) val sichergestellt ist, 3 Im Gegensatz zu Java beschränkt Scalas protected die Sichtbarkeit allerdings auf abgeleitete Klassen. Lasse Kristopher Meyer 10 3 Ausgewählte Konzepte dass die Werte zwar abrufbar, aber nicht veränderbar sind. In Scala werden Objekte, deren Zustand sich nicht ändern kann, als funktionale Objekte bezeichnet. In den Zeilen 8 und 10 wird nun anschließend die Methode + in Operatorenschreibweise definiert und überladen, um die Addition mit reellen und komplexen Zahlen zu implementieren. Tatsächlich gewinnen wir durch diese Überladung sogar mehr. Beispielsweise lassen sich unsere komplexen Zahlen nun auch ohne Probleme mit ganzen Zahlen (vom Typ Int) addieren: scala> z + 1 Created: 2.0 + 1.0i res0: Complex = 2.0 + 1.0i Diese Berechnung funktioniert, weil Scala eine implizite Konvertierung des Typs Int zum Typ Double vornimmt. Eine Methodenüberladung reicht also aus, um unsere komplexen Zahlen mit sich selbst und allen anderen numerischen Typen (Byte, Short, Int, ...) addieren zu können.4 Die Methode toString überschreibt schließlich die gleichnamige Methode in der Klasse Any, der Wurzel der Scala-Klassenhierarchie, von der jede Klasse direkt oder indirekt erbt. Der overrideModifikator muss beim Überschreiben einer konkreten Methode zwingend gesetzt werden, damit Überschreibungsfehler vom Compiler erkannt werden können. Vererbung wird wie in Java mit dem Schlüsselwort extends gekennzeichnet. Ohne Angabe der extends-Klausel ergänzt der Compiler implizit die Klasse AnyRef als Superklasse. AnyRef ist Scalas Pendant zu Javas java.lang.Object und neben AnyVal (der Superklasse der Werttypen Byte, Short, Int, ...) die einzige direkte Subklasse von Any. In Listing 3.4 leiten wir die Klasse Programmer von der abstrakten Klasse Employee ab. Klassen, die abstract deklariert sind, dürfen abstrakte Attribut- oder Methodendeklarationen enthalten. Falls abstrakte Deklarationen vorhanden sind, müssen diese allerdings in abgeleiteten, konkreten Klassen implementiert werden, ansonsten würde der Compiler eine Fehlermeldung ausgeben. 1 2 3 4 5 6 7 8 abstract class Employee(val name: String) { def work() override def toString = name } // abstract method // concrete method class Programmer(name: String) extends Employee(name) { def work() { println(name + " programs some software.") } } // concrete method Listing 3.4: Abstrakte Klassen und Vererbung Die Klasse Programmer erbt von Employee also das Attribut name und die konkrete Methode toString (insbesondere weil diese Elemente public sind) und implementiert die abstrakte Methode work. Bei der Instanziierung einer Klasse müssen auch immer die Attribute aller Superklassen initialisiert werden. Da Konstruktoren in Scala nicht mitvererbt werden, muss deshalb immer ein expliziter Konstruktor der Superklasse angegeben werden, selbst wenn diese abstrakt ist. An diesen Konstruktor wird in Zeile 6 der Klassenparameter name weitergereicht. Würden wir name ebenfalls als val kennzeichnen, so würde der Compiler ein neues Attribut name in Programmer anlegen. Da das Attribut in der Klasse Employee aber konkret ist (es wird ja auf jeden Fall initialisiert) würden wir dieses damit überschreiben. Diese Überschreibung müsste dann explizit gekennzeichnet werden: 4 Dabei ist allerdings die Reihenfolge der Operanden wichtig. Im umgekehrten Fall 1 + z wäre die Definition einer neuen impliziten Konvertierung vom Typ Int zum Typ Complex notwendig. Lasse Kristopher Meyer 11 3 Ausgewählte Konzepte class Programmer(override val name: String) extends Employee(name) { ... } Ohne die Kennzeichnung eines Klassenparameters als val oder var werden allerdings keine Attribute angelegt. Der Parameter wird hier also einfach an den Konstruktor weitergereicht. Das Konkretisieren neuer Subklassen (von abstrakten Klassen) enspricht dem Prinzip des Polymorphismus (subtyping polymorphism). Wie erwartet können wir die Subklassen der abstrakten Klasse Employee wie gewohnt instanziieren und benutzen: scala> val steve = new Programmer("Steve") steve: Programmer = Steve scala> steve.name res0: String = Steve scala> steve.work() Steve programs some software. 3.2.2 Singleton-Objekte Da Scala rein objektorientiert ist können Klassen keine statischen Attribute oder Methoden haben. Stattdessen lassen sich in Scala mit dem Schlüsselwort object Singleton-Objekte definieren, auf deren Attribute und Methoden über die Java-übliche Notation für statics zugegriffen werden kann (z. B. Thread.yield()). Werden ein Singleton-Objekt und eine Klasse mit gleichem Namen in derselben Quelldatei definiert, so bezeichnet man das Objekt als companion object der Klasse und die Klasse als companion class des Singleton-Objekts: 1 2 3 object Complex { def apply(real: Double, imag: Double) = new Complex(real, imag) } // factory method Listing 3.5: Complex.scala (2. Teil) Listing 3.5 ergänzt die Datei Complex.scala aus Listing 3.3 um ein companion object mit der Methode apply. Diese Methode wird als factory method bezeichnet, da sie bei der Instanziierung einer Klasse ohne das Schlüsselwort new implizit aufgerufen wird: val z = Complex(4.0, 2.0) // calls Complex.apply(...) Eine Klasse und ihr companion object haben gegenseitigen Zugriff auf private Attribute und Methoden. Auf diese Weise können wie in Java statische Bestandteile einer Klasse erzeugt werden. Wie das Schlüsselwort object bereits andeutet, sind Singleton-Objekte selbst auch First-ClassObjekte. Sie können beispielsweise andere Klassen erweitern, Methoden erben und überall dort eingesetzt werden, wo der Typ der Superklasse erwartet wird: 1 2 3 4 5 6 7 8 abstract class Planet(val name: String) { def collideWith(that: Planet) { println("Oh no! The " + this.name + " collides with the " + that.name + "!") } } object Earth extends Planet("Earth") object Sun extends Planet("Sun") Listing 3.6: Singleton-Objekte und Vererbung Lasse Kristopher Meyer 12 3 Ausgewählte Konzepte Die Singleton-Objekte aus Listing 3.6 können wie gewöhnliche Objekte benutzt werden: scala> Earth.collideWith(Sun) Oh no! The Earth collides with the Sun! Singleton-Objekte ohne companion class werden als standalone object bezeichnet. Im Gegensatz zu Klassen definieren standalone objects keinen neuen Typ. Ein Beispiel für Singleton-Objekte ohne companion class haben wir bereits in Listing 3.1 gesehen. Die Hauptmethode main wird üblicherweise als Einstiegspunkt in einem standalone object definiert. 3.2.3 Traits Traits (Eigenschaften, Charakteristiken) sind wiederverwendbare Sammlungen von Attributund Methodendefinitionen, welche Klassen und Objekten beigemischt werden können. Traits sind grob vergleichbar mit Javas Interfaces (einer Klasse können mehrere Traits beigemischt werden, Traits definieren einen Typ, ...), mit dem Unterschied, dass in Traits, genau wie in Klassen, auch konkrete Attribute und Methoden definiert werden dürfen. Die Definition eines Traits wird mit dem Schlüsselwort trait eingeleitet, das Beimischen erfolgt über extends (bei Klassen ohne explizite Superklasse) oder with (bei Klassen mit expliziter Superklasse oder Objekten): 1 2 3 4 5 6 7 trait ScalaSkills extends Employee { val areaOfExpertise: String def skills() { println(this + " likes programming in Scala.") } } // abstract field // concrete method class ScalaProgrammer(name: String, val areaOfExpertise: String) extends Programmer(name) with ScalaSkills Listing 3.7: Abstrakte und konkrete Elemente in Traits Erweitert ein Trait eine explizite Klasse (hier Employee aus Listing 3.4), so kann es nur in deren Subklassen (-objekte) gemischt werden. Das Beimischen eines Traits ist semantisch gleichbedeutend mit dem Erben von einer abstrakten Klasse: Konkrete Elemente werden vererbt (oder überschrieben) und abstrakte Elemente müssen implementiert werden. In Listing 3.7 erbt ScalaProgrammer bspw. skills und implementiert areaOfExpertise (wegen val in Zeile 6). Die Verwendung konkreter Methoden erlaubt das Definieren umfangreicherer Schnittstellen, die die redundanten Implementierungen identischer Methoden in mehreren Klassen des Traits vermeiden. Neben der Möglichkeit, einer Klasse mehrere Traits beizumischen, gibt es einen weiteren wichtigen Unterschied zur Vererbung: super-Aufrufe sind nicht statisch, sondern dynamisch gebunden. Dieser Umstand ermöglicht sogenannte stapelbare Modifikationen für Objekte. 1 2 3 4 5 6 trait Motivation extends Employee { abstract override def work() { println(this + " is motivated!"); super.work() } } trait Laziness extends Employee { abstract override def work() { println(this + " is lazy..."); super.work() } } Listing 3.8: Stapelbare Modifikationen In Listing 3.8 definieren wir zwei Traits, die verschiedene Modifikationen für die abstrakte Methode work zur Verfügung stellen. Der entscheidende Punkt ist, dass der super-Aufruf vom Compiler nicht statisch gebunden wird. Stattdessen wird die aufzurufende Implementierung von work bei jedem Beimischen in eine konkrete Klasse zur Laufzeit neu ermittelt. Der super-Aufruf Lasse Kristopher Meyer 13 3 Ausgewählte Konzepte funktioniert deshalb nur, wenn das Trait nach einem Trait oder einer Klasse mit der konkreten Implementierung der Methode work beigemischt wird. Diesen Sachverhalt teilt man dem Compiler über die Modifikatorenkombination abstract override mit, die nur bei Traits erlaubt ist. Wir erhalten nun die Möglichkeit (stapelbare) Modifikationen an Objekten und ihren Methoden vorzunehmen, indem wir die Traits in bestimmter Reihenfolge beimischen: scala> val bill = new Programmer("Bill") with Motivation with Laziness bill: Programmer with Motivation with Laziness = Bill scala> bill.work() Bill is lazy... Bill is motivated! Bill programs some software. Mehrere abstrakte super-Aufrufe werden linearisiert gebunden. Angefangen beim am weitesten rechts beigemischten Trait Laziness wird der Aufruf von work über Motivation bis zur implementierenden Klasse Programmer weitergereicht. Die Linearisierung ist somit auch der Grund dafür, dass die typischen Probleme der Mehrfachvererbung (wie beispielsweise das DiamantProblem) bei der Verwendung von Traits vermieden werden. 3.3 Funktionale Konzepte 3.3.1 Grundlagen, Funktionen und Closures Wie wir in Abschnitt 3.2 gesehen haben, können Funktionen, wie in Java, als Methoden innerhalb von Klassen definiert werden. Das Funktionskonzept in Scala ist allerdings allgemeiner. Scalas Funktionen sind First-Class-Objekte. Wie andere Werttypen (Double, Int, ...) dürfen Funktionen in Form von unbenannten Literalen im Quelltext verwendet werden: val multiply = (x: Double, y: Double) => x * y Funktionsliterale werden vom Compiler in Klassen umgewandelt, die zur Laufzeit instanziiert werden. Das erzeugte Objekt bezeichnet man als Funktionswert. Funktionswerte sind gleichberechtigte Objekte, die man, wie im obigen Beispiel, in Variablen ablegen oder als Argumente an andere Funktionen übergeben kann: scala> List(1, 2, 3).foreach((x: Int) => print(x)) 123 scala> Array(1, 2, 3).filter((x: Int) => x > 1) res0: Array[Int] = Array(2, 3) Viele Funktionen höherer Ordnung, die aus funktionalen Programmiersprachen wie Haskell bekannt sind, wurden auch in Scalas Standardbibliothek implementiert. Dynamische Listen werden außerdem durch die Klasse List aus dem Paket scala.collection.immutable realisiert: val l = "a" :: "b" :: "c" :: Nil // is equivalent to ’val l = List("a", "b", "c")’ Hierbei repräsentiert das Singleton-Objekt Nil die leere Liste. Durch target typing und eine spezielle Platzhalter-Syntax sind auch kürzere Schreibweisen für Funktionsargumente möglich: val a = Array(1, 2, 3).filter(x => x > 1) val b = Array(1, 2, 3).filter(_ > 1) // target typing // placeholder syntax Lasse Kristopher Meyer 14 3 Ausgewählte Konzepte Im ersten Fall erkennt der Compiler, dass x vom Typ Int sein muss, da auf einem Array[Int] operiert wird (target typing). Der Platzhalter _ im zweiten Fall ist immer dann möglich, wenn ein oder mehrere Parameter des Funktionsliterals in gleicher Reihenfolge im Rumpf der Funktion auftauchen.5 Der Unterstrich kann auch bei einzelnen Argumenten oder kompletten Argumentlisten benutzt werden. So erhält man partiell angewandte Funktionen: def concat(x: String, y: String) = x + y val a = concat _ // a("Hello, ", "World") returns "Hello, world!" val b = concat(_: String, "world!") // b("Hello, ") returns "Hello, world!" Ein weiteres Einsatzgebiet des Platzhalters _ ist die funktionale Programmiertechnik Currying. Beim Currying werden Funktionen auf mehrere Argumentlisten angewendet, statt nur auf eine einzige. Auf diese Weise können Funktionen mit mehreren Argumenten auf Funktionen mit einer geringeren Anzahl an Argumemten reduziert werden: def curriedConcat(x: String)(y: String) = x + y Durch die Platzhalter-Syntax erhalten wir hier wieder eine partiell angewandte Funktion: scala> curriedConcat("Hello, ")("world!") res0: String = Hello, world! scala> val a = curriedConcat("Hello, ") _ a: String => String = <function1> scala> a("world") res1: String = Hello, world Funktionsliterale sind nicht nur auf die Verwendung der Funktionsparameter beschränkt. Es ist ebenso erlaubt sich auf Variablen aus dem Deklarationskontext oder lokale Variablen anderer Funktionen zu beziehen. Funktionswerte solcher Funktionsliterale werden Closures genannt: 1 2 3 4 var globalFactor = 2 // globalFactor is reassignable val multiply = (x: Int) => x * globalFactor def makeMultiplier(localFactor: Int) = (x: Int) => x * localFactor Listing 3.9: Closures Aus Sicht der Funktionen in multiply und makeMultiplier sind die Variablen globalFactor und localFactor in Listing 3.9 freie Variablen. Im ersten Fall hängt das Ergebnis des angewandten Funktionsliterals vom aktuellen Wert der var-Variable globalFactor ab: scala> multiply(21) res0: Int = 42 scala> globalFactor = 4 globalFactor: Int = 4 scala> multiply(21) res1: Int = 84 Im zweiten Fall referenziert das Closure jeweils eine Kopie der lokalen Variable localFactor: scala> val multiplyByTwo = makeMultiplier(2) multiplyByTwo: Int => Int = <function1> 5 Ist kein target typing möglich, müssen Typen allerdings explizit angegeben werden (z. B.: „(_: Int) > 1“). Lasse Kristopher Meyer 15 3 Ausgewählte Konzepte scala> multiplyByTwo(21) res3: Int = 42 Eine weitere Eigenschaft Scalas in Bezug auf funktionale Programmierung ist der Umgang des Compilers mit endrekursiven Funktionen (tail recursive functions): 1 2 3 4 5 6 def sumOuter(n: Int) = { def sumInner(m: Int, n: Int): Int = if (n == 0) m else sumInner(m + n, n - 1) // tail recursion sumInner(0, n) } Listing 3.10: Endrekursive Funktionen Die Funktion sumOuter in Listing 3.10 berechnet die Summe der ersten n natürlichen Zahlen. Zu diesem Zweck enthält sie die innere, endrekursive Hilfsfunktion sumInner.6 Der Scala-Compiler nutzt nun die Tatsache, dass sich jede endrekursive Funktion durch eine Iteration ausdrücken lässt, und optimiert die Funktion sumInner derart, dass beim rekursiven Aufruf kein neuer Stack Frame erzeugt wird. Obige Funktion ist also genauso effizient wie eine iterative Lösung. 3.3.2 Case Classes, generische Typen und Pattern Matching Case Classes und Pattern Matching sind Konstrukte, die das Definieren und Verarbeiten ungekapselter, meist rekursiver Datenstrukturen erlauben. Case Classes ergänzen herkömmliche Klassen um einige syntaktische Annehmlichkeiten, die insbesondere unkompliziertes Pattern Matching auf Objekte erlauben. Die Kennzeichnung einer Klasse mit dem Modifikator case ergänzt diese um: • eine Fabrikmethode mit dem Namen der Klasse (s. Abschnitt 3.2.2). • implizite val-Präfixe für alle Klassenparameter, also automatisches Anlegen der Attribute. • automatische Implementierungen der Methoden toString, hashCode, equals und copy. Case Classes erlauben beispielsweise die einfache Definition rekursiver algebraischer Datentypen, wie man sie aus funktionalen Programmiersprachen (vgl. z. B. Haskells data) gewohnt ist: 1 2 3 abstract class Tree[+T] case object Empty extends Tree[Nothing] case class Node[T](left: Tree[T], value: T, right: Tree[T]) extends Tree[T] Listing 3.11: Tree.scala (1. Teil) Listing 3.11 zeigt eine Möglichkeit Binärbäume in Scala zu implementieren. In Zeile 1 definieren wir dazu zunächst den repräsentativen generischen Typ Tree[T]. Der Typparameter T bestimmt den Typ der Werte, die in den Knoten unseres Binärbaums gespeichert sind. Durch das Präfix + legen wir außerdem fest, dass für alle Subtypen U von T der Typ Tree[U] ebenfalls ein Subtyp von Tree[T] ist (Kovarianz; Kontravarianz wird mit - gekennzeichnet). Diese Eigenschaft ist notwendig, da der leere Baum Empty in Zeile 2 vom Typ Tree[Nothing] ist. Die Klasse Nothing ist Subklasse aller Klassen T, also kann Empty überall verwendet werden, wo ein Tree[T] erwartet wird. Knoten realisieren wir mit der Case Class Node[T], für die der Compiler implizit die Attribute left, value und right anlegt. 6 Für rekursive Funktionen muss immer der Typ des Rückgabewerts angegeben werden. Lasse Kristopher Meyer 16 3 Ausgewählte Konzepte Als nächstes definieren wir mittels Pattern Matching die polymorphe Funktion map[T, U], die eine Funktion f: T => U auf alle Knoten eines Tree[T] anwendet und einen Tree[U] erzeugt: 1 2 3 4 def map[T, U](f: T => U, t: Tree[T]): Tree[U] = t match { case Empty => Empty case Node(l, v, r) => Node(map(f, l), f(v), map(f, r)) } Listing 3.12: Tree.scala (2. Teil) Pattern Matching wird über den match-Ausdruck realisiert, der einer Verallgemeinerung des switch-Statements aus Java entspricht. Währrend switch nur Konstanten in patterns unterstützt, sind in match auch variable patterns (z. B. l, v und r in Zeile 3), wildcard patterns (dargestellt durch _) und constructor patterns (wie Empty und Node(l, v, r)) erlaubt. Im obigen Fall überprüft match die Struktur von t auf das erste passende Pattern und wertet den entsprechenden Ausdruck hinter dem => aus. Wir können map[T, U] wie folgt verwenden: scala> map((_: Int) + 1, Empty) res0: Tree[Int] = Empty scala> map((_: Int) > 1, Node(Node(Empty, 1, Empty), 2, Node(Empty, 3, Empty))) res1: Tree[Boolean] = Node(Node(Empty,false,Empty),true,Node(Empty,true,Empty)) Obwohl map[T, U] durch die Typen T und U parametrisiert ist, ist die Angabe dieser Typparameter optional, da der Compiler die entsprechenden Typen durch die Typen der konkreten Parameter f und t inferieren kann. 3.3.3 Aktoren und Nebenläufigkeit Da Scala uneingeschränkt auf alle Java-Bibliotheken zugreifen kann, ist es grundsätzlich möglich Nebenläufigkeit mit Javas Threading-Modell oder anderen Konzepten aus der Java-Bibliothek java.util.concurrent zu realisieren. Um jedoch die Probleme von Nebenläufigkeitsmodellen mit geteilten Speicherbereichen (Locks, Synchronisation, Zugriffskontrolle auf kritische Bereiche, ...) zu vermeiden, stellt Scala zusätzlich ein eher funktionales Konzept zur nebenläufigen Programmierung zur Verfügung: das Aktor-Modell. Scalas Aktor-Modell ist eine Implementierung des Aktor-Modells der funktionalen Programmiersprache Erlang, in der Aktoren ein fester Sprachbestandteil sind. In Scala werden Aktoren in der Bibliothek scala.actors definiert. Aktoren sind Thread-ähnliche Entitäten mit eigenem Speicherbereich, die zusätzlich über eine Mailbox verfügen, mit der sie Nachrichten von anderen Aktoren empfangen können. Aktoren können auf zweierlei Arten implementiert werden. Die erste Möglichkeit ist das Erweitern des Traits Actor und die Implementierung der abstrakten Methode act: 1 2 3 4 5 6 7 8 9 10 import scala.actors._ object ReceivingActor extends Actor { def act() { println("I’m acting!") while (true) { receive { case message => println("Received message: " + message) } } } } Listing 3.13: Aktoren implementieren (Das Actor-Trait) Lasse Kristopher Meyer 17 3 Ausgewählte Konzepte Durch den Aufruf der Methode start wird der Aktor in Form eines neuen Threads gestartet: scala> ReceivingActor.start() I’m acting! res0: scala.actors.Actor = ReceivingActor$@734ee775 Die zweite Möglichkeit ist das Verwenden der Hilfsmethode actor aus dem Objekt Actor: 1 2 3 4 5 6 7 import scala.actors.Actor._ val forwardingActor = actor { while (true) { receive { case message => ReceivingActor ! message } } } Listing 3.14: Aktoren implementieren (Die actor-Methode) Auf diese Weise wird der Aktor sofort gestartet. Die Methode receive überprüft die Mailbox eines Aktoren auf ungelesene Nachrichten, die einem der angegeben Pattern (case-Fälle) entsprechen. Wird eine solche Nachricht gefunden, wird die entsprechende Aktion ausgeführt. Andernfalls blockiert der Aktor, bis eine passende Nachricht eingeht. Nachrichten werden mit der Methode ! an den aufrufenden Aktoren versendet: scala> forwardingActor ! "Send this to ReceivingActor!" scala> Received message: Send this to ReceivingActor! Wie man sieht leitet der Aktor forwardingActor jede eingehende Nachricht an den Aktor ReceivingActor weiter, der diese wiederum in die Standardausgabe schreibt. Beide Aktoren sind unabhängige, nebenläufige Threads, die nur über Nachrichten kommunizieren können. Lasse Kristopher Meyer 18 4 Technische Unterstützung 4 Technische Unterstützung Dieses Kapitel beschäftigt sich mit der Kompatibilität zu Java und den technischen Hilfsmitteln zur Programmiersprache Scala wie dem Compiler, dem Interpreter und kompatiblen Entwicklungsumgebungen. Im letzten Abschnitt wird abschließend der Typesafe Stack erläutert. 4.1 Integration mit Java Wie bereits in Abschnitt 2.1 erwähnt, war die Interoperabilität mit Java ein wichtiges Ziel bei der Entwicklung Scalas. Deshalb werden Scala-Programme in Java-Bytecode übersetzt und laufen auf der Java Virtual Machine.1 Das hat zur Konsequenz, dass Scala-Programme üblicherweise dieselbe Laufzeit-Performance aufweisen wie Java-Programme [OSV10]. Außerdem ist Scala somit für alle Plattformen verfügbar, für die eine JVM existiert — Scala ist deshalb ebenso plattformunabhängig wie Java. Die vollständige Kompatibilität mit Java ermöglicht Scala Zugriff auf alle Java-Bibliotheken (Methodenaufrufe, Zugriff auf Attribute, Vererbung und die Implementierung von Interfaces funktionieren wie gewohnt). Umgekehrt kann Scala-Code auch in Java-Programme eingebettet werden und von diesen fast uneingeschränkt benutzt werden. 4.2 Compiler, Interpreter, IDEs Neben dem eigentlichen Sprachkern enthält die Standard-Scala-Distribution auch den ScalaCompiler scalac, den interaktiven Interpreter scala (s. Abbildung 4.1) und die StandardBibliotheken. Die meistverwendete und empfohlene integrierte Entwicklungsumgebung für Scala ist die Scala IDE für Eclipse. Diese bietet die für Java bekannten Funktionalitäten wie Syntax-Highlighting, Autovervollständigung und Abhängigkeitsverwaltung (auch mit Java-Projekten). Neben der Scala IDE für Eclipse gibt es noch Plugins für die Entwicklungsumgebungen IntelliJ IDEA und NetBeans. Außerdem kann Scala in die meisten gängigen Abbildung 4.1: Der Scala-Interpreter Editoren integriert werden. Es sind beispielsweise Plugins für Emacs, jEdit, Notepad-Plus, TextMate, SubEthaEdit und Textwrangler vorhanden. Manche neuere Editoren wie Sublime Text 2 unterstützen Scala bereits von Haus aus. 1 Zusätzlich wurde Scala auch für die .NET-Plattform implementiert, die im Vergleich aber geringeren aktiven Support erfährt. Lasse Kristopher Meyer 19 4 Technische Unterstützung 4.3 Der Typesafe Stack Der Typesafe Stack ist eine von der Firma Typesafe, Inc. kostenlos zur Verfügung gestellte, quelloffene und plattformunabhängige Software-Plattform, die im Wesentlichen aus den drei Komponenten Scala (und den zugehörigen Bibliotheken), Akka (ereignisgesteuerte Middleware für die Entwicklung verteilter und nebenläufiger Anwendungen in Scala und Java) und dem Play Framework besteht und als vollständige Entwicklungs- und Verteilungsumgebung für ScalaEntwickler dienen soll. Zu diesem Zweck werden dem Entwickler neben den drei Hauptkomponenten viele weitere Tools wie die Scala IDE für Eclipse und das Simple Build Tool (auch sbt; quelloffenes Build Tool für Scala-Projekte, ähnlich Apache Ant) zur Verfügung gestellt. Der Typesafe Stack kann kostenpflichtig (mit der Typesafe Subscription) um kommerziellen Support bei der Produktion von Anwendungen, professionelle Wartung und zusätzliche Entwicklertools ergänzt werden. Momentan ist der Typesafe Stack (in der Version 2.0.2) für alle gängigen Betriebssysteme (Mac OS X, Linux und Windows) erhältlich. Lasse Kristopher Meyer 20 5 Diskussion und Zusammenfassung 5 Diskussion und Zusammenfassung Als „general purpose programming language“ hatte die Entwicklung Scalas vor allem das Ziel eine Sprache zu schaffen, die in vielfältigen Einsatzgebieten Anwendung findet. Die zunehmende Verwendung Scalas in verschiedensten (auch industriellen) Bereichen [TIO13] scheint dieser Erwartungshaltung gerecht zu werden. Tatsächlich bietet Scala eine endlos erscheinende Menge an Funktionen für fast alle erdenklichen Anwendungsfälle. Daraus resultiert allerdings auch eine anfangs schwer überschaubare Sprachkomplexität, die gerade beim Einstieg abschreckend wirkt und Scala seit Beginn der Entwicklung in Verruf bringt, eine eher „akademische Sprache“ [The10] zu sein. Im Vergleich zu Java-Programmen erfordert das Lesen und Verstehen von ScalaProgrammen beispielsweise mehr Hintergrundwissen über Methodensignaturen, Funktionalität von Operatoren oder Typen. Methoden in Operatorenschreibweise und Typinferenz machen den Code zwar prägnanter und kürzer als vergleichbarer Java-Code, sie sind aber auch schwieriger zu verstehen als beschreibende Methodennamen oder explizite Typangaben. Die vermeintliche Komplexität resultiert wohl vor allem aus der konsequenten Verknüpfung objektorientierter und funktionaler Programmierung, die gleichzeitig auch Scalas größte Stärke ist. Denn sie bietet die Möglichkeit, die schon bei Java geschätzten Vorteile objektorientierter Programmierung (wie die Strukturierung von Code oder die Wiederverwendbarkeit von Programmkomponenten durch Vererbung) mit dem hohen Abstraktionsniveau funktionaler Programmierung zu verbinden. Dies führt dazu, dass das Programmieren in Scala wiederum wesentlich intuitiver ist als in Java. Obwohl die funktionalen Konzepte ungewohnt scheinen, ermöglichen sie sehr ausdrucksstarke Programmierung und eröffnen einem völlig neue Möglichkeiten zu Problemlösungen. Zusätzlich machen sie den Code sehr robust und sicher. Eine weitere Stärke der Programmiersprache Scala ist die Skalierbarkeit, die sich schon in der Unterstützung durch die umfangreichen Standardbibliotheken zeigt. Scala lässt sich relativ einfach um gewünschte Funktionalitäten erweitern, ohne dass sich dabei das Gefühl nativer Sprachunterstützung verliert. Deshalb ist Scala auch sehr gut in Bereichen einsetzbar, in denen nicht der gesamte Umfang der Sprache gefragt ist, sondern nur bestimmte Funktionalitäten und Schnittstellen zur Verfügung gestellt werden müssen. Zusammenfassend eignet sich Scala sowohl für kleine Skripte, als auch für Projekte beliebiger Größe. Zwar erscheint Scala vorerst sehr komplex, profitiert aber enorm aus dem multiparadigmatischen Ansatz: Scala-Programme weisen oft einen hohen Abstraktionsgrad auf, sind kompakt und prägnant, strukturiert, kompatibel und portabel. Trotz der eher langsam zunehmenden Popularität der Sprache und in Hinblick auf die Tatsache, dass Scala ursprünglich auch als ein besseres Java konzipiert war, stellt sich allerdings die Frage, welche Bedeutung Scala in Zukunft haben wird. Da sich Scala noch immer in aktiver Entwicklung befindet und noch längst keinen finalen Status erreicht hat, erscheint folgende Formulierung von Neal Gafter (ehemaliger Entwickler der Programmiersprache Java) jedoch treffend: „Will Scala be the next great language? Only time will tell. Martin Odersky’s team certainly has the taste and skill for the job. One thing is sure: Scala sets a new standard against which future languages will be measured.“ [OSV10] Lasse Kristopher Meyer 21 Literaturverzeichnis Literaturverzeichnis [Cam11] C AMEL C ASE C ON: Deutschland : http:// _ _ www.camelcasecon.de/pdf/scala/Auswertung der Scala_Umfrage.pdf, 2011. – Auswertung der Scala-Umfrage 2011. Letzter Abruf: 11.01.2013 [Eck11] E CKEL, Bruce: Scala: The Static Language that Feels Dynamic. USA : http:// www.artima.com/weblogs/viewpost.jsp?thread=328540, 2011. – Letzter Abruf: 10.01.2013 [JPPV09] J ENSON, Steve ; PAYNE, Alex ; P OINTER, Robey ; V ENNERS, Bill: Twitter on Scala. USA : http://www.artima.com/scalazine/articles/twitter_on_scala.html, 2009. – Letzter Abruf: 10.01.2013 [Ode06] O DERSKY, Martin: A Brief History of Scala. USA : http://www.artima.com/weblogs/ viewpost.jsp?thread=163733, 2006. – Letzter Abruf: 08. Januar 2013 [OSV10] O DERSKY, Martin ; S POON, Lex ; V ENNERS, Bill: Programming in Scala: A comprehensive step-by-step guide. Second Edition. USA : Artima, Inc., 2010. – ISBN 0–9815316–4–4, 978–0–9815316–4–9 [OV09] O DERSKY, Martin ; V ENNERS, Bill: The Origins of Scala: A Conversation with Martin Odersky, Part I. USA : http://www.artima.com/scalazine/articles/origins_of_ scala.html, 2009. – Letzter Abruf: 08. Januar 2013 [Pla11] P LAY F RAMEWORK: Introducing Play 2.0. http://www.playframework.org/ documentation/2.0.4/Philosophy, 2011. – Letzter Abruf: 10.01.2013 [Red12] R ED M ONK: The RedMonk Programming Language Rankings: September 2012. USA : http://redmonk.com/sogrady/2012/09/12/language-rankings-9-12/, 2012. – Letzter Abruf: 16.01.2013 [The10] T HE al F LYING F ROG language”. B LOG: ”Scala is foremost an industri- http://flyingfrogblog.blogspot.de/2010/08/ scala-is-foremost-industrial-language.html, 2010. – Letzter Abruf: 23.01.2013 [TIO13] TIOBE S OFTWARE: TIOBE Programming Community Index for January 2013. Niederlande : http://www.tiobe.com/index.php/content/paperinfo/tpci/, 2013. – Letzter Abruf: 09.01.2013 [Typ] T YPESAFE , I NC .: Scala: The Language of Pragmatism. USA, Schweiz, Schweden : http: //typesafe.com/technology/scala, . – Letzter Abruf: 12.01.2013 [Typ12] T YPESAFE , I NC .: Scala in the Enterprise. USA, Schweiz, Schweden : http://www. scala-lang.org/node/1658#Sony, 2012. – Letzter Abruf: 16.01.2013 Lasse Kristopher Meyer