Die multiparadigmatische Programmiersprache

Werbung
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
Herunterladen