Kapitel 1. Von Null auf Sechzig: Einführung in Scala
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Beginnen wir mit einem kurzen Blick darauf, warum du dich mit Scala beschäftigen solltest. Dann werden wir eintauchen und etwas Code schreiben.
Warum Scala?
Scala ist eine Sprache, die auf die Bedürfnisse des modernen Softwareentwicklers zugeschnitten ist. Sie ist eine statisch typisierte, objektorientierte und funktionale Mischplattformsprache mit einer prägnanten, eleganten und flexiblen Syntax, einem ausgeklügelten Typensystem und Idiomen, die die Skalierbarkeit von kleinen Tools bis hin zu großen, anspruchsvollen Anwendungen fördern. Betrachten wir also jede dieser Ideen im Detail:
- Eine Java Virtual Machine (JVM), JavaScript und eine eigene Sprache
-
Scala begann als JVM-Sprache, die die Leistung und Optimierungen der JVM sowie das reichhaltige Ökosystem von Tools und Bibliotheken rund um Java nutzt. In jüngerer Zeit bringt Scala.js Scala zu JavaScript und Scala Native kompiliert Scala zu nativem Maschinencode und umgeht so die JVM und JavaScript-Laufzeiten.
- Objektorientierte Programmierung
-
Scala unterstützt vollständig die objektorientierte Programmierung (OOP). Scala Traits bieten eine saubere Möglichkeit, Code mit Mixin-Komposition zu implementieren. Scala bietet bequeme und vertraute OOP für alle Typen, sogar für numerische Typen, und ermöglicht gleichzeitig eine hochperformante Codegenerierung.
- Funktionale Programmierung
-
Scala unterstützt die funktionale Programmierung (FP) vollständig. FP hat sich als das beste Werkzeug erwiesen, um Probleme mit Parallelität, Big Data und allgemeiner Code-Korrektheit zu lösen. Unveränderliche Werte, Funktionen erster Klasse, Code ohne Seiteneffekte und funktionale Sammlungen tragen alle zu prägnantem, leistungsstarkem und korrektem Code bei.
- Ein ausgeklügeltes Typensystem mit statischer Typisierung
-
Das reichhaltige, statische Typsystem von Scala trägt wesentlich zu fehlerfreiem Code bei, in dem Fehler schon beim Kompilieren erkannt werden. Dank der Typinferenz ist Scala-Code oft genauso prägnant wie Code in dynamisch typisierten Sprachen, aber von Natur aus sicherer.
- Eine prägnante, elegante und flexible Syntax
-
Ausführliche Ausdrücke in anderen Sprachen werden in Scala zu prägnanten Idiomen. Scala bietet verschiedene Möglichkeiten, um domänenspezifische Sprachen (DSLs) zu entwickeln, APIs, die sich für die Benutzerinnen und Benutzer wie eigene Sprachen anfühlen.
- Skalierbare Architekturen
-
Du kannst in Scala winzige Tools für eine einzige Datei schreiben, aber auch große, verteilte Anwendungen.
Der Name Scala ist eine Verkürzung der Worte skalierbare Sprache. Er wird scah-lah ausgesprochen, wie das italienische Wort für Treppe. Daher werden die beiden a's gleich ausgesprochen.
Scala wurde 2001 von Martin Odersky ins Leben gerufen. Die erste öffentliche Version wurde am 20. Januar 2004 veröffentlicht. Martin Odersky ist Professor an der Fakultät für Informatik und Kommunikationswissenschaften an der École Polytechnique Fédérale de Lausanne (EPFL). Während seiner Studienzeit arbeitete er in der Gruppe von Niklaus Wirth, der für Pascal bekannt ist. Martin arbeitete an Pizza, einer frühen funktionalen Sprache auf der JVM. Später arbeitete er zusammen mit Philip Wadler, einem der Entwickler von Haskell, an GJ, einem Prototyp für die spätere Generik in Java. Martin wurde von Sun Microsystems angeworben, um die Referenzimplementierung von javac
mit Generics zu entwickeln, dem Vorläufer des Java-Compilers, der heute mit dem Java Developer Kit (JDK) ausgeliefert wird.
Die Anziehungskraft von Scala
Die wachsende Zahl von Scala-Nutzern seit ihrer Einführung vor über 15 Jahren bestätigt meine Ansicht, dass Scala eine Sprache für unsere Zeit ist. Du kannst die Reife der JVM- und JavaScript-Ökosysteme nutzen und gleichzeitig modernste Sprachfunktionen mit einer prägnanten und dennoch ausdrucksstarken Syntax nutzen, um die heutigen Entwicklungsherausforderungen zu meistern.
In jedem Bereich brauchen Profis ausgefeilte, leistungsstarke Werkzeuge und Techniken. Es kann eine Weile dauern, bis du sie beherrschst, aber du gibst dir Mühe, denn die Beherrschung ist der Schlüssel zu deiner Produktivität und deinem Erfolg.
Ich glaube, dass Scala eine Sprache für professionelle Entwickler ist. Natürlich sind nicht alle Scala-Benutzer Profis, aber Scala ist die Art von Sprache, die ein Profi in unserem Bereich braucht: reich an Funktionen, hoch performant und ausdrucksstark für eine breite Klasse von Problemen. Es wird eine Weile dauern, bis du Scala beherrschst, aber wenn du es geschafft hast, wirst du dich von deiner Programmiersprache nicht mehr eingeengt fühlen.
Warum Scala 3?
Wenn du bisher Scala benutzt hast, hast du Scala 2 benutzt, die Hauptversion seit März 2006! Scala 3 zielt darauf ab, Scala in mehrfacher Hinsicht zu verbessern.
Erstens stärkt Scala 3 die Grundlagen von Scala, insbesondere das Typsystem. Martin Odersky und seine Mitarbeiter haben den DOT-Kalkül ( Dependent Object Typing ) entwickelt, der eine solidere Grundlage für das Typsystem von Scala bildet. In Scala 3 ist DOT integriert.
Zweitens hat Scala 2 viele leistungsstarke Funktionen, die aber manchmal schwer zu benutzen sind. Scala 3 verbessert die Benutzerfreundlichkeit und Sicherheit dieser Funktionen, insbesondere der Implikate. Andere Warzen und Rätsel der Sprache wurden beseitigt.
Drittens verbessert Scala 3 die Konsistenz und Ausdruckskraft der Sprachkonstrukte von Scala und entfernt unwichtige Konstrukte, um die Sprache kleiner und regelmäßiger zu machen. Außerdem wird der bisherige experimentelle Ansatz für Makros durch einen neuen, prinzipiellen Ansatz für die Metaprogrammierung ersetzt.
Wir werden diese Änderungen benennen, wenn wir die entsprechenden Sprachfunktionen erkunden.
Umstellung auf Scala 3
Das Scala-Team hat hart daran gearbeitet, den Umstieg von Scala 2 auf Scala 3 so einfach wie möglich zu gestalten und gleichzeitig der Sprache Verbesserungen zu ermöglichen, die bahnbrechende Änderungen erfordern. Scala 3 verwendet dieselbe Standardbibliothek wie Scala 2.13, wodurch eine Reihe von Änderungen, die du sonst bei einem Upgrade an deinem Code vornehmen müsstest, wegfallen. Daher empfehle ich, falls nötig, zuerst auf Scala 2.13 zu aktualisieren, um die Standardbibliothek zu nutzen, und dann auf Scala 3 zu aktualisieren.
Um den Übergang zu den Sprachänderungen so schmerzlos wie möglich zu gestalten, gibt es verschiedene Möglichkeiten, Scala 3 Code zu kompilieren, der veraltete Scala 2 Konstrukte erlaubt oder verbietet. Es gibt sogar Compiler-Flags, die den Code automatisch für dich umschreiben! Siehe "Scala 3 Versionen" und "Das scalac Kommandozeilentool" in Kapitel 22 für weitere Informationen.
Eine vollständige Anleitung zur Migration auf Scala 3 findest du im Scala Center's Scala 3 Migration Guide.
Installieren der benötigten Scala-Tools
Es gibt viele Möglichkeiten, Werkzeuge zu installieren und Scala-Projekte zu erstellen. In Kapitel 22 und auf der Scala-Website unter Getting Started findest du weitere Informationen zu den verfügbaren Tools und Optionen für den Einstieg in Scala. Hier werde ich mich auf die einfachste Art und Weise konzentrieren, die Werkzeuge zu installieren, die für den Beispielcode in diesem Buch benötigt werden.
Die Beispiele richten sich an Scala 3 für die JVM. Auf den Scala.js- und Scala Native-Webseiten findest du Informationen über die Verwendung dieser Plattformen.
Du musst nur zwei Tools installieren:
-
Ein aktuelles Java JDK, Version 8 oder neuer. Empfohlen werden neuere Langzeitversionen wie Version 11 oder 15 (die neueste Version zum Zeitpunkt der Erstellung dieses Artikels).
-
sbt
das De-facto-Build-Tool für Scala.
Befolge die Anweisungen zur Installation des JDK und sbt
auf den jeweiligen Websites.
Wenn wir den Befehl sbt
in "Using sbt" verwenden , bootet er alles, was sonst noch benötigt wird, einschließlich des scalac
Compilers und des scala
Tools zum Ausführen von Code.
Aufbau der Code-Beispiele
Jetzt, wo du die Werkzeuge hast, die du brauchst, kannst du die Codebeispiele herunterladen und bauen:
- Hol dir den Code
-
Lade die Code-Beispiele herunter, wie in "Code-Beispiele erhalten" beschrieben .
- Start
sbt
-
Öffne ein Terminal und wechsle in das Stammverzeichnis für die Codebeispiele. Gib den Befehl
sbt test
ein, um alle benötigten Bibliotheksabhängigkeiten herunterzuladen, einschließlich des Scala-Compilers. Das wird eine Weile dauern. Dann kompiliertsbt
den Code und führt die Unit-Tests aus. Du wirst eine Menge Ausgaben sehen, die mit einer "Erfolgsmeldung" enden. Wenn du den Befehl erneut ausführst, sollte er sehr schnell beendet sein, weil er nichts mehr tun muss.
Herzlichen Glückwunsch! Du bist bereit, loszulegen.
Tipp
Die meiste Zeit des Buches werden wir die Scala-Tools indirekt über sbt
nutzen. Diese Software lädt automatisch die Scala-Bibliothek und die Tools herunter, die wir benötigen, einschließlich der erforderlichen Abhängigkeiten von Drittanbietern.
Mehr Tipps
Es ist nützlich, in deinem Browser ein Lesezeichen für die Scaladoc-Dokumentation der Scala-Standardbibliothek zu setzen. Wenn ich einen Typ in der Bibliothek erwähne, füge ich oft einen Link zu dem entsprechenden Scaladoc-Eintrag hinzu, damit du es leichter hast.
Verwende das Suchfeld oben auf der Seite, um schnell etwas in den Dokumentationen zu finden. Die Dokumentationsseite für jeden Typ enthält einen Link, über den du den entsprechenden Quellcode in Scalas GitHub-Repository einsehen kannst. Das ist eine gute Möglichkeit, um zu erfahren, wie die Bibliothek implementiert wurde. Achte auf den Link "Quelle".
Für die Arbeit mit den Beispielen ist jeder Texteditor oder jede integrierte Entwicklungsumgebung (IDE) ausreichend. Scala-Plug-ins gibt es für alle gängigen Editoren und IDEs, wie IntelliJ IDEA und Visual Studio Code. Sobald du das benötigte Scala-Plug-in installiert hast, können die meisten Umgebungen dein sbt
Projekt öffnen und importieren automatisch alle benötigten Informationen, wie die Scala-Version und die Bibliotheksabhängigkeiten.
Die Unterstützung für Scala in vielen IDEs und Texteditoren basiert mittlerweile auf dem Language Server Protocol (LSP), einem offenen Standard, der von Microsoft ins Leben gerufen wurde. Das Metals-Projekt implementiert LSP für Scala. Auf der Metals-Website findest du Details zur Installation für deine IDE oder deinen Editor. Im Allgemeinen ist die Community deines bevorzugten Editors oder deiner IDE die beste Quelle für aktuelle Informationen zur Scala-Unterstützung.
Tipp
Wenn du gerne mit Scala-Arbeitsblättern arbeitest, können viele der Codebeispiele in Arbeitsblätter umgewandelt werden. Details dazu findest du in der README der Codebeispiele.
sbt verwenden
Wir werden uns mit den Grundlagen von sbt
befassen, mit denen wir die Codebeispiele erstellen und bearbeiten werden.
Wenn du sbt
startest, startet sbt
eine interaktive Shell, wenn du keine Aufgabe angibst, die ausgeführt werden soll. Probieren wir das mal aus und sehen uns ein paar der verfügbaren Aufgaben an.
In der folgenden Auflistung steht $
für die Eingabeaufforderung der Shell (z.B. bash
, zsh
oder die Windows-Befehlsshell), in der du den Befehl sbt
startest, >
ist die standardmäßige interaktive Eingabeaufforderung sbt
und #
startet einen Kommentar. Du kannst die meisten dieser Befehle in beliebiger Reihenfolge eingeben:
$
sbt >help
# Describe commands.
> tasks# Show the most commonly used, available tasks.
> tasks -V# Show ALL the available tasks.
> compile# Incrementally compile the code.
>test
# Incrementally compile the code and run the tests.
> clean# Delete all build artifacts.
> console# Start the interactive Scala environment.
> run# Run one of the "main" methods (applications) in the project.
> show x# Show the value of setting or task "x".
>exit
# Quit the sbt shell (also control-d works).
Das sbt
Projekt für die Codebeispiele ist so konfiguriert, dass die Eingabeaufforderung sbt
angezeigt wird:
sbt:programming-scala-3rd-ed-code-examples>
Um Platz zu sparen, verwende ich die prägnantere Eingabeaufforderung >
, wenn ich sbt
Sitzungen anzeige.
Tipp
Eine praktische sbt
Technik ist es, jedem Befehl eine Tilde, ~
, voranzustellen. Jedes Mal, wenn Änderungen an der Datei auf der Festplatte gespeichert werden, wird der Befehl erneut ausgeführt. Ich verwende ~test
zum Beispiel ständig, um meinen Code zu kompilieren und meine Tests auszuführen. Da sbt
einen inkrementellen Compiler verwendet, musst du nicht jedes Mal auf einen vollständigen Neuaufbau warten. Um diese Schleifen zu verlassen, drückst du Return.
Der Befehl scala
CLI hat eine integrierte REPL (read, eval, print, loop). Dies ist ein historischer Begriff, der auf LISP zurückgeht. Er ist genauer als Interpreter, der manchmal verwendet wird. Scala-Code wird nicht interpretiert. Er wird immer kompiliert und dann ausgeführt, auch wenn du die interaktive REPL verwendest, bei der einzelne Codeabschnitte eingegeben und ausgeführt werden. Daher verwende ich den Begriff REPL, wenn ich mich auf diese Verwendung der scala
CLI beziehe. Du kannst sie mit dem Befehl console
in sbt
aufrufen. Wir werden das oft tun, um mit den Codebeispielen in diesem Buch zu arbeiten. Die Eingabeaufforderung der Scala REPL lautet scala>
. Wenn du diese Eingabeaufforderung in den Codebeispielen siehst, verwende ich die REPL.
Bevor du die REPL startest, baut sbt console
dein Projekt und richtet den Klassenpfad mit deinen kompilierten Artefakten und abhängigen Bibliotheken ein. Dieser Komfort bedeutet, dass du die scala
REPL nur selten außerhalb von sbt
verwenden kannst, weil du den Klassenpfad selbst einrichten müsstest.
Um die Shell sbt
zu verlassen, benutze exit
oder Strg-D. Um die Scala REPL zu verlassen, benutze :quit
oder Ctrl-D.
Tipp
Die Scala REPL ist eine sehr effektive Methode, um mit Code-Idiomen zu experimentieren und eine API zu lernen, auch wenn es sich nicht um eine Scala-API handelt. Wenn du sie von sbt
aus mit der Aufgabe console
aufrufst, werden die Projektabhängigkeiten und der kompilierte Projektcode dem Klassenpfad für die REPL hinzugefügt.
Ich habe die Compileroptionen für die Codebeispiele (in build.sbt
) so konfiguriert, dass sie -source:future
. Dieses Flag bewirkt, dass Warnungen für Konstrukte ausgegeben werden, die in Scala 3.0 noch erlaubt sind, aber in Scala 3.1 entfernt werden oder veraltet sind und in einer späteren Version entfernt werden sollen. Ich werde konkrete Beispiele für geplante Übergänge nennen, sobald wir sie finden. Es gibt mehrere Sprachversionen, die mit der Option -source
Option verwendet werden können. Siehe "Scala 3 Versionen" für weitere Informationen), vor allem, wenn du mit der Migration deines eigenen Codes nach Scala 3 beginnst.
Ausführen der Scala-Kommandozeilen-Tools mit sbt
Wenn die Scala 3-Befehlszeilen-Tools separat installiert werden (siehe "Befehlszeilen-Schnittstellen-Tools" für weitere Informationen), heißt der Scala-Compiler scalac
und die REPL heißt scala
. Wir werden sie von sbt
ausführen lassen, aber ich zeige dir auch, wie du sie direkt ausführen kannst.
Lass uns ein einfaches Scala-Programm ausführen. Betrachte dieses "Skript" aus den Codebeispielen:
// src/script/scala/progscala3/introscala/Upper1.scala
class
Upper1
:
def
convert
(
strings
:
Seq
[
String
]):
Seq
[
String
]
=
strings
.
map
((
s
:
String
)
=>
s
.
toUpperCase
)
val
up
=
new
Upper1
()
val
uppers
=
up
.
convert
(
List
(
"Hello"
,
"World!"
))
println
(
uppers
)
Tipp
Die meisten Auflistungen, wie auch diese, beginnen mit einem Kommentar, der den Dateipfad in den Codebeispielen enthält, damit du die Datei leicht finden kannst. Nicht alle Beispiele enthalten Dateien, aber wenn du eine Auflistung ohne Pfadkommentar siehst, macht sie oft dort weiter, wo die vorherige Auflistung aufgehört hat.
Ich werde die Details dieses Codes in Kürze erklären, aber konzentrieren wir uns jetzt darauf, ihn auszuführen.
Wechsle dein aktuelles Arbeitsverzeichnis in das Stammverzeichnis der Codebeispiele. Starte sbt
und führe die Aufgabe console
aus. Verwende dann den Befehl :load
, um den Inhalt der Datei zu kompilieren und auszuführen. Im nächsten Listing ist $
die Eingabeaufforderung des Terminals, >
die Eingabeaufforderung von sbt
, scala>
die Eingabeaufforderung der Scala REPL und die Ellipsen (...) stehen für unterdrückte Ausgaben:
$
sbt ... > console ... scala> :load src/script/scala/progscala3/introscala/Upper1.scala List(
HELLO, WORLD!)
...
Und damit haben wir die oberste Richtlinie der Gilde der Programmierbuchautoren erfüllt, die besagt, dass unser erstes Programm "Hello World!" ausgeben muss.
Alle Codebeispiele, die wir in der REPL verwenden werden, haben Pfade, die mit src/script beginnen. In den meisten Fällen kannst du jedoch den Code aus einer beliebigen Quelldatei kopieren und in die Eingabeaufforderung der REPL einfügen.
Wenn du die scala
REPL für Scala separat installiert hast, kannst du scala
an der Eingabeaufforderung im Terminal eingeben, anstatt die separaten Schritte sbt
und console
. Die meisten der Beispielskripte werden jedoch nicht mit scala
außerhalb von sbt
laufen, weil sbt console
die Bibliotheken und den kompilierten Code im Klassenpfad enthält, die die meisten Skripte benötigen.1
Hier ist eine vollständigere REPL-Sitzung, damit du einen Eindruck davon bekommst, was du tun kannst. Hier kombiniere ich sbt
und console
in einem Schritt (einige Ausgaben wurden weggelassen):
$
sbt console ... scala> :help The REPL has several commands available: :help print this summary :load <path> interpret lines in a file :quitexit
the REPL :type <expression> evaluate thetype
of the given expression :doc <expression> print the documentationfor
the given expression :imports show importhistory
:reset reset the REPL to its initial state, ... scala> vals
=
"Hello, World!"
val s:String
=
Hello, World! scala> println(
"Hello, World!"
)
Hello, World! scala>1
+ 2 val res0:Int
=
3 scala> s.con<tab> concat contains containsSlice contentEquals scala> s.contains(
"el"
)
val res1:Boolean
=
true
scala> :quit$
# back at the terminal prompt. "Control-D" also works.
Die Liste der verfügbaren Befehle und die Ausgabe von :help
kann sich zwischen verschiedenen Scala-Versionen ändern.
Wir haben eine Zeichenkette, "Hello, World!"
, einer Variablen namens s
zugewiesen, die wir mit dem Schlüsselwort val
als unveränderlichen Wert deklariert haben. Die println
Methode gibt einen String auf der Konsole aus, gefolgt von einem Zeilenvorschub.
Als wir zwei Zahlen addiert haben, haben wir das Ergebnis keiner Variablen zugewiesen, also hat das REPL einen Namen für uns erfunden, res0
, den wir in nachfolgenden Ausdrücken verwenden können.
Die REPL unterstützt die Tabulatorvervollständigung. Die gezeigte Eingabe wird verwendet, um anzuzeigen, dass nach s.con
ein Tabulator getippt wurde. Die REPL antwortete mit einer Liste von Methoden auf String
, die aufgerufen werden können. Der Ausdruck wurde mit einem Aufruf der Methode contains
abgeschlossen.
Der Typ von etwas wird durch seinen Namen, einen Doppelpunkt und dann den Typ angegeben. Wir haben hier keine expliziten Angaben zum Typ gemacht, weil die Typen abgeleitet werden können. Wenn du Typen explizit angibst oder wenn sie gefolgert und für dich angezeigt werden, nennt man sie Typdeklarationen.2 Die Ausgabe der REPL zeigt mehrere Beispiele.
Wenn ein Typ zu einer Deklaration hinzugefügt wird, wird zum Beispiel die Syntax name: String
anstelle von String name
verwendet. Letzteres wäre in Scala aufgrund der Typinferenz mehrdeutig, da Typinformationen im Code weggelassen werden können, aber vom Compiler abgeleitet werden.
Tipp
Die Anzeige der Typen in der REPL ist sehr praktisch, um die Typen zu lernen, die Scala für bestimmte Ausdrücke herleitet. Das ist ein Beispiel für die Erkundung, die die REPL ermöglicht.
Weitere Informationen zu den Befehlszeilen-Tools, wie z.B. die Verwendung der scala
CLI, um kompilierten Code außerhalb von sbt
auszuführen, findest du unter "Befehlszeilen-Tools".
Ein Geschmack von Scala
Wir haben bereits ein wenig von Scala gesehen, als wir über Werkzeuge gesprochen haben, z. B. wie man "Hello World!" ausgibt. Der Rest dieses Kapitels und die beiden folgenden Kapitel bieten eine schnelle Tour durch die Funktionen von Scala. Dabei werden wir gerade genug Details besprechen, um zu verstehen, was vor sich geht, aber viele der tieferen Hintergrunddetails müssen auf spätere Kapitel warten. Sieh diese Tour als eine Einführung in die Scala-Syntax und als einen Vorgeschmack darauf, wie das Programmieren in Scala im Alltag aussieht.
Tipp
Wenn ich einen Typ in der Scala-Bibliothek einführe, findest du seinen Eintrag im Scaladoc. Scala 3 verwendet die Scala 2.13 Bibliothek mit ein paar kleinen Ergänzungen.
Scala folgt gängigen Kommentar-Konventionen. A // comment
geht bis zum Ende einer Zeile, während ein /*
comment
*/
Zeilengrenzen überschreiten kann. Kommentare, die in die Scaladoc-Dokumentation aufgenommen werden sollen, verwenden /**
comment
*/
.
Dateien mit dem Namen src/test/scala/.../*Suite.scala sind Tests, die mit MUnit geschrieben wurden (siehe "Testwerkzeuge"). Um alle Tests auszuführen, verwende den Befehl sbt
test
. Um nur einen bestimmten Test auszuführen, verwende testOnly path
, wobei path
der voll qualifizierte Typname für den Test ist:
>
testOnly
progscala3
.
objectsystem
.
equality
.
EqualitySuite
[
info
]
Compiling
1
Scala
source
to
...
progscala3
.
objectsystem
.
equality
.
EqualitySuite
:
+
The
==
operator
is
implemented
with
the
equals
method
0.01
s
+
The
!=
operator
is
implemented
with
the
equals
method
0.001
s
...
[
info
]
Passed
:
Total
14
,
Failed
0
,
Errors
0
,
Passed
14
[
success
]
Total
time
:
1
s
,
completed
Feb
29
,
2020
,
5
:
00
:
41
PM
>
Die entsprechende Quelldatei ist src/test/scala/progscala3/objectsystem/equality/EqualitySuite.scala. Beachte, dass sbt
den Konventionen von Apache Maven folgt, wonach die Verzeichnisse für den kompilierten Quellcode unter src/main/scala und die Tests unter src/test/scala abgelegt werden. Danach folgt die Paketdefinition, progscala3.objectsystem.equality, die dem Dateipfad progscala3/objectsystem/equality entspricht. Pakete organisieren den Code hierarchisch. Der Test innerhalb der Datei ist als Klasse mit dem Namen EqualitySuite
.
Hinweis
Scala-Pakete, -Namen und -Dateiorganisation folgen größtenteils den Java-Konventionen. Java verlangt, dass der Verzeichnispfad und der Dateiname mit dem deklarierten Paket und einer einzelnen öffentlichen Klasse innerhalb der Datei übereinstimmen müssen. Scala schreibt die Einhaltung dieser Regeln nicht vor, aber es ist üblich, sie zu befolgen, besonders bei größeren Codebasen. Die Codebeispiele folgen diesen Konventionen.
Schließlich definieren viele der Dateien unter src/main/scala Einstiegspunkte (wie main
Methoden), die Ausgangspunkte für die Ausführung dieser kleinen Anwendungen. Du kannst sie auf eine von mehreren Arten ausführen.
Verwende zunächst den Befehl sbt
' run
. Er findet alle Einstiegspunkte und fordert dich auf, einen davon auszuwählen. Beachte, dass sbt
nur src/main/scala und src/main/java durchsucht. Wenn du kompilierst und Tests ausführst, werden src/test/scala und src/test/java durchsucht. Das src/script wird von sbt
ignoriert.
Nehmen wir ein weiteres Beispiel, das wir später im Kapitel untersuchen werden, src/main/scala/progscala3/introscala/UpperMain2.scala. Rufe run hello world
auf, wobei run
die Aufgabe sbt
ist und hello world
beliebige Argumente sind, die an ein Programm übergeben werden, das wir aus der Liste auswählen, die für uns ausgedruckt wird (über 50 Möglichkeiten!). Gib die Nummer ein, die für progscala3.introscala.Hello2
angezeigt wird:
> run hello world ... Multiple main classes detected, select one to run: ... [38] progscala3.introscala.Hello2 ... 38 <--- What you type! [info] running progscala3.introscala.Hello2 hello world HELLO WORLD [success] Total time: 2 s, completed Feb 29, 2020, 5:08:18 PM
Dieses Programm wandelt die Eingabeargumente in Großbuchstaben um und gibt sie aus.
Eine zweite Möglichkeit, dieses Programm auszuführen, ist die Verwendung von runMain
und die Angabe desselben vollqualifizierten Pfads zur Hauptklasse, der gezeigt wurde, progscala3.introscala.Hello2
. Dadurch wird die Eingabeaufforderung übersprungen:
> runMain progscala3.introscala.Hello2 hello world again! ... [info] running progscala3.introscala.Hello2 HELLO WORLD AGAIN! [success] Total time: 0 s, completed Feb 29, 2020, 5:18:05 PM >
Dieser Code ist bereits kompiliert, du kannst ihn also auch außerhalb von sbt
mit dem Befehl scala
ausführen. Jetzt muss der richtige Klassenpfad angegeben werden, einschließlich aller Abhängigkeiten. In diesem Beispiel ist es ganz einfach: Der Klassenpfad muss nur das Stammverzeichnis für alle kompilierten .class
Dateien enthalten. Ich verwende hier eine Shell-Variable, damit die Zeile in den Platz passt; ändere die 3.0.0
entsprechend der tatsächlich verwendeten Scala-Version:3
$ cp
=
"target/scala-3.0.0/classes/"
$
scala -classpath$cp
progscala3.introscala.Hello2 Hello Scala World! HELLO SCALA WORLD!
Es gibt eine letzte Alternative, die wir nutzen können. Wie wir gleich sehen werden, definiert UpperMain2.scala
einen einzigen Einstiegspunkt. Aus diesem Grund kann der Befehl scala
die Quelldatei direkt laden, kompilieren und in einem Schritt ausführen, ohne dass vorher ein scalac
Schritt notwendig ist. Das Argument -classpath
brauchen wir jetzt nicht mehr, aber wir müssen die Datei anstelle des voll qualifizierten Namens angeben, den wir bisher verwendet haben:
$
scala src/main/scala/progscala3/introscala/UpperMain2.scala Hello World!
HELLO WORLD!
Schauen wir uns die Umsetzungen dieser Beispiele an. Zuerst ist hier wieder Upper1.scala
:
// src/script/scala/progscala3/introscala/Upper1.scala
class
Upper1
:
def
convert
(
strings
:
Seq
[
String
]):
Seq
[
String
]
=
strings
.
map
((
s
:
String
)
=>
s
.
toUpperCase
)
val
up
=
new
Upper1
()
val
uppers
=
up
.
convert
(
List
(
"Hello"
,
"World!"
))
println
(
uppers
)
Wir deklarieren eine Klasse, Upper1
, mit dem Schlüsselwort class
, gefolgt von einem Doppelpunkt (:
). Der gesamte Klassenkörper wird in den nächsten beiden Zeilen eingerückt.
Hinweis
Wenn du Scala bereits kennst, fragst du dich vielleicht, warum es keine geschweiften Klammern {
...}
gibt und warum ein Doppelpunkt (:
) nach dem Namen Upper1
steht? Ich verwende die neue Syntax für optionale geschweifte Klammern, die ich in "Die neue Scala 3-Syntax - Optionale Klammern" näher erläutern werde .
Upper1
enthält eine Methode namens convert
. Methodendefinitionen beginnen mit dem Schlüsselwort def
, gefolgt von dem Methodennamen und einer optionalen Parameterliste. Die Methodensignatur endet mit einem optionalen Rückgabetyp. Der Rückgabetyp kann in vielen Fällen abgeleitet werden, aber die explizite Angabe des Rückgabetyps, wie hier gezeigt, bietet eine nützliche Dokumentation für den Leser und vermeidet außerdem gelegentliche Überraschungen bei der Typinferenz.
Hinweis
Mit Parametern bezeichne ich die Liste der Dinge, die eine Methode erwartet, wenn du sie aufrufst. Mit Argumenten bezeichne ich die Werte, die du beim Aufruf der Methode tatsächlich übergibst.
Typdefinitionen werden in der Syntax name: type
angegeben. Die Parameterliste ist strings: Seq[String]
und der Rückgabetyp der Methode ist Seq[String]
, nach der Parameterliste.
Ein Gleichheitszeichen (=
) trennt die Methodensignatur vom Methodenrumpf. Warum ein Gleichheitszeichen?
Ein Grund dafür ist, Mehrdeutigkeit zu vermeiden. Wenn du den Rückgabetyp weglässt, kann Scala ihn ableiten. Wenn die Methode keine Parameter benötigt, kannst du auch die Klammern weglassen. Das Gleichheitszeichen macht das Parsen also eindeutig, wenn eines oder beide dieser Merkmale weggelassen werden. Es ist klar, wo die Signatur endet und der Methodenkörper beginnt.
Die Methode convert
nimmt eine Folge (Seq
) von null oder mehr Eingabestrings und gibt eine neue Sequenz zurück, in der jeder der Eingabestrings in Großbuchstaben umgewandelt wird. Seq
ist eine Abstraktion für Sammlungen, durch die du iterieren kannst. Die tatsächliche Art der Sequenz, die von dieser Methode zurückgegeben wird, ist die gleiche Art, die als Argument übergeben wurde, wie Vector
oder List
(beides sind unveränderliche Sammlungen).
Auflistungstypen wie Seq[T]
sind parametrisierte Typen, wobei T
der Typ der Elemente in der Sequenz ist. Scala verwendet eckige Klammern ([
...]
) für parametrisierte Typen, während einige andere Sprachen spitze Klammern (<
...>
) verwenden.
List[T]
ist eine unveränderliche verknüpfte Liste. Der Zugriff auf den Kopf einer List
ist O(1), während der Zugriff auf ein beliebiges Element an Position N O(N) ist. Vector[T]
ist ein Subtyp von Seq[T]
mit fast O(1) für alle Zugriffsmuster.
Hinweis
Scala erlaubt die Verwendung von spitzen Klammern in Bezeichnern, wie Methoden- und Variablennamen. Es ist zum Beispiel üblich, eine "kleiner als"-Methode zu definieren und sie <
zu nennen. Um Mehrdeutigkeit zu vermeiden, behält Scala eckige Klammern für parametrisierte Typen vor, damit Zeichen wie <
und >
als Bezeichner verwendet werden können.
Im Hauptteil von convert
verwenden wir die Methode map
, um über die Elemente zu iterieren, eine Transformation auf jedes Element anzuwenden und dann eine neue Sammlung mit den Ergebnissen zu erstellen.
Die Funktion, die an die Methode map
übergeben wird, um die Transformation durchzuführen, ist ein unbenanntes (anonymes) Funktionsliteral der Form (parameters) => body
:
(
s
:
String
)
=>
s
.
toUpperCase
Sie nimmt eine Parameterliste mit einem einzigen String
namens s
entgegen. Der Körper des Funktionsliters steht nach dem "Pfeil" =>
. Der Körper ruft die Methode toUpperCase
auf s
auf.4 Das Ergebnis dieses Aufrufs wird automatisch von dem Funktionsliteral zurückgegeben. In Scala ist der letzte Ausdruck in einer Funktion, Methode oder einem anderen Block der Rückgabewert. (Das Schlüsselwort return
gibt es in Scala, aber es kann nur in Methoden verwendet werden, nicht in anonymen Funktionen wie dieser hier. Es wird nur für frühe Rückgaben in der Mitte von Methoden verwendet).
Auf der JVM werden Funktionen mit JVM-Lambdas implementiert, wie dir die REPL anzeigt:
scala
>
(
s
:
String
)
=>
s
.
toUpperCase
val
res0
:
String
=>
String
=
Lambda$7775
/
0x00000008035fc040
@
7673711
e
=
Beachte, dass das REPL diese Funktion wie jeden anderen Wert behandelt und ihr einen synthetischen Namen, res0
, gibt, wenn du selbst keinen angibst (z. B. val f = (s: String) => s.toUpperCase
). Schreibgeschützte Werte werden mit dem Schlüsselwort val
deklariert.
Zurück zu Upper1.scala
, die letzten beiden Zeilen, die sich außerhalb der Klassendefinition befinden, erzeugen eine Instanz von Upper1
mit dem Namen up
, unter Verwendung von new Upper1()
. Dann wird up
verwendet, um zwei Zeichenketten in Großbuchstaben umzuwandeln. Schließlich wird die resultierende Folge uppers
mit println
ausgegeben. Normalerweise erwartet println
ein einzelnes String-Argument, aber wenn du ihm ein Objekt übergibst, wie Seq
, wird die Methode toString
aufgerufen. Wenn du sbt console
aufrufst und dann den Inhalt der Datei Upper1.scala
kopierst und einfügst, wird dir das REPL mitteilen, dass der tatsächliche Typ von Seq[String]
List[String]
(eine verknüpfte Liste) ist.
src/script/.../Upper1.scala ist also für das Kopieren und Einfügen (oder die Verwendung von :load
) in der REPL gedacht. Schauen wir uns eine andere Implementierung an, die kompiliert und dann ausgeführt wird. Ich habe Main
zum Dateinamen der Quelldatei hinzugefügt. Beachte, dass der Pfad zur Quelldatei jetzt src/main anstelle von src/script enthält:
// src/main/scala/progscala3/introscala/UpperMain1.scala
package
progscala3
.
introscala
object
UpperMain1
:
def
main
(
params
:
Array
[
String
]
)
:
Unit
=
(
"UpperMain1.main: "
)
params
.
map
(
s
=>
s
.
toUpperCase
)
.
foreach
(
s
=>
printf
(
"%s "
,
s
)
)
println
(
""
)
def
main
(
params
:
Array
[
String
]
)
:
Unit
=
(
"main: "
)
params
.
map
(
s
=>
s
.
toUpperCase
)
.
foreach
(
s
=>
printf
(
"%s "
,
s
)
)
println
(
""
)
@main
def
Hello
(
params
:
String
*
)
:
Unit
=
(
"Hello: "
)
params
.
map
(
s
=>
s
.
toUpperCase
)
.
foreach
(
s
=>
printf
(
"%s "
,
s
)
)
println
(
""
)
Deklariere den Ort des Pakets,
progscala3.introscala
.Deklariere eine
main
Methode, einen Programmeinstiegspunkt, innerhalb einerobject
. Ich werde gleich erklären, was eineobject
ist.Deklariere einen alternativen
main
Einstiegspunkt als Top-Level-Methode, die außerhalb vonobject
liegt, aber auf das aktuelle Paket,progscala3.introscala
, beschränkt ist.Deklariere eine Einstiegsmethode, bei der wir einen anderen Namen verwenden können und flexiblere Optionen für die Argumentliste haben.
Pakete funktionieren ähnlich wie in anderen Sprachen. Sie stellen einen Namensraum zur Verfügung, in dem die Deklarationen und der Zugriff auf sie abgelegt werden. Hier befinden sich die Deklarationen im Paket progscala3.introscala
.
Du hast wahrscheinlich schon in anderen Sprachen Klassen gesehen, die Mitglieder kapseln, d.h. Methoden, Felder (oder Attribute), die den Status enthalten, und so weiter. In vielen Sprachen ist der Einstiegspunkt, an dem das Programm beginnt, eine main
Methode. In Java wird diese Methode innerhalb einer Klasse definiert und als static
deklariert, d.h. sie ist nicht an eine bestimmte Instanz gebunden. Du kannst jede statische Definition mit der Syntax UpperMain1.main
referenzieren, um unser Beispiel zu verwenden.
Das Muster der statischen Deklarationen in Klassen ist so allgegenwärtig, dass Scala es in die Sprache einbaut. Stattdessen deklarieren wir ein object
UpperMain1
, indem wir das Schlüsselwort object
verwenden. Dann deklarieren wir main
und andere Mitglieder mit der gleichen Syntax, die wir auch in Klassen verwenden würden. Das Schlüsselwort static
gibt es in Scala nicht.
Diese Datei hat drei Einstiegspunkte. Der erste, UpperMain1.main
, ist ein Beispiel dafür, wie man in Scala 2 Einstiegspunkte deklariert. Gemäß den Java-Konventionen ist der Name main
erforderlich und er wird mit einem Array[String]
Parameter für die vom Benutzer angegebenen Argumente deklariert, auch wenn das Programm keine Argumente oder bestimmte Argumente in einer bestimmten Reihenfolge annimmt, z. B. eine ganze Zahl gefolgt von zwei Strings. Du musst dich um das Parsen der Argumente kümmern. Außerdem sind Array
in Scala veränderbar, was eine Quelle von Fehlern sein kann. Die Verwendung unveränderlicher Argumente ist von Natur aus sicherer. All diese Probleme werden im letzten Einstiegspunkt, Hello
, behandelt, auf den wir gleich noch eingehen werden.
Innerhalb von UpperMain1.main
geben wir zuerst den Namen der Methode aus (ohne Zeilenumbruch), was nützlich ist, um zu vergleichen, wie diese drei Einstiegspunkte aufgerufen werden. Dann werden die Eingabeargumente (params
) in Großbuchstaben umgewandelt und eine neue Sammlung zurückgegeben. Schließlich verwenden wir eine weitere Sammlungsmethode namens foreach
, um die neue Sammlung zu durchlaufen und jede Zeichenkette mit printf
zu drucken. Diese Methode erwartet eine Formatierungszeichenkette und Argumente, hier s
, um die endgültige Zeichenkette zusammenzustellen.5
Lass uns mit UpperMain1.main
laufen:
> runMain progscala3.introscala.UpperMain1 Hello World! UpperMain1.main: HELLO WORLD! >
Die Methode main
selbst ist nicht Teil des qualifizierten Pfades, sondern nur das umschließende Objekt UpperMain1
.
Scala 3 führt zwei neue Funktionen für mehr Flexibilität ein. Erstens kannst du Methoden, Variablen usw. außerhalb von Objekten und Klassen deklarieren. So wird die zweite Methode main
deklariert, aber ansonsten funktioniert sie wie UpperMain1.main
. Sie wird anders gescoped, wie wir sehen können, wenn wir sie verwenden:
> runMain progscala3.introscala.UpperMain1$package
Hello World!
main: HELLO WORLD!
>
Beachte, dass die Definition auf das Paket und den Dateinamen des Quellcodes beschränkt ist! Wenn du die Datei in etwas wie FooBar.scala umbenennst und neu kompilierst, wird der Befehl runMain progscala3.introscala.FooBar$package
.... Wenn du die Quelldatei zum Geltungsbereich hinzufügst, vermeidest du Kollisionen mit anderen Definitionen im selben Paketbereich, aber mit anderen Quelldateien. Allerdings ist $package
im Namen für Linux- und macOS-Shells wie bash
unpraktisch, daher empfehle ich nicht, einen Einstiegspunkt auf diese Weise zu definieren.
Stattdessen empfehle ich die zweite, neue Funktion von Scala 3, eine alternative Art der Definition von Einstiegspunkten, wie unser dritter Einstiegspunkt, die Methode Hello
, zeigt. Die Annotation @main
kennzeichnet diese Methode als Einstiegspunkt. Beachte, wie wir auf sie verweisen, wenn wir sie ausführen:
> runMain progscala3.introscala.Hello Hello World! Hello: HELLO WORLD! >
Jetzt wird der Methodenname verwendet. Normalerweise benennt man keine Methoden, die mit einem Großbuchstaben beginnen, aber für Einstiegspunkte ist es nützlich, wenn die Aufrufbefehle ähnlich aussehen sollen wie Java-Aufrufe wie java
...progscala3.introscala.Hello
.... Hello
der auch außerhalb eines Objekts deklariert wird, aber das ist nicht erforderlich.
Die neuen @main
Einstiegspunkte haben mehrere Vorteile. Sie reduzieren den Verwaltungsaufwand bei der Definition. Sie können mit Parameterlisten definiert werden, die den erwarteten Argumenten entsprechen, z. B. Sequenzen, Strings und ganze Zahlen. Hier wollen wir null oder mehr String-Argumente. Das *
in params: String*
bedeutet null oder mehr String
s (so genannte wiederholte Parameter), die an den Methodenkörper übergeben werden, wo params
mit einem unveränderlichen Seq[String]
implementiert wird. Veränderbare Array
s werden vermieden.
Beachte, dass der Typ des Rückgabewerts aller drei Methoden Unit
ist. Betrachte Unit
als Analogon zu void
in anderen Sprachen, was bedeutet, dass nichts Nützliches zurückgegeben wird.
Hinweis
Da in dieser Datei drei Einstiegspunkte definiert sind, können wir scala
nicht verwenden, um diese Datei in einem Schritt zu analysieren und auszuführen. Deshalb habe ich stattdessen UpperMain2
verwendet. Wir werden uns diese Datei gleich ansehen und sehen, dass sie nur einen einzigen Einstiegspunkt hat.
Wenn du UpperMain1
als object
deklarierst, wird es zu einem Singleton, d.h. es wird immer nur eine Instanz davon geben, die die Scala-Laufzeitumgebung für uns erstellt. Mit new
kannst du keine eigenen Instanzen erstellen.
Scala macht das Singleton-Designmuster zu einem erstklassigen Mitglied der Sprache. In den meisten Fällen sind diese object
Deklarationen genau wie andere class
Deklarationen, aber sie werden verwendet, wenn du nur eine einzige Instanz brauchst, um einige Methoden und Felder zu halten, im Gegensatz zu der Situation, in der du mehrere Instanzen brauchst, jede mit Feldern mit eindeutigen Werten pro Instanz und Methoden, die jeweils auf eine einzige Instanz wirken.
Das Singleton-Designmuster hat Nachteile. Es ist schwierig, eine Singleton-Instanz in Unit-Tests durch ein Test-Double zu ersetzen, und wenn alle Berechnungen über eine einzige Instanz laufen, gibt es Bedenken hinsichtlich der Thread-Sicherheit und der Skalierbarkeit. Wir werden in diesem Buch jedoch viele Beispiele sehen, in denen Objekte effektiv eingesetzt werden.
Tipp
Um Verwirrung zu vermeiden, verwende ich Instanz statt Objekt, wenn ich mich auf eine Instanz beziehe, die aus einer Klasse mit new
oder der einzelnen Instanz eines object
erstellt wurde. Weil Klassen und Objekte so ähnlich sind, verwende ich für sie den generischen Begriff Typ. Alle Typen, die wir sehen werden, wie String
, sind als Klassen oder Objekte implementiert.
Zurück zu den Details der Implementierung: Beachte die Funktion, die wir an map
übergeben haben:
s
=>
s
.
toUpperCase
In unserem vorherigen Beispiel wurde (s: String) => s.toUpper(s)
verwendet. Die meiste Zeit kann Scala die Typen der Parameter für Funktionsliterale ableiten, weil der von map
bereitgestellte Kontext dem Compiler sagt, welchen Typ er erwarten soll. Die Typdeklaration String
wird also nicht benötigt.
Die Methode foreach
wird verwendet, wenn wir jedes Element verarbeiten und nur Seiteneffekte ausführen wollen, ohne einen neuen Wert zurückzugeben. Hier geben wir einen String auf der Standardausgabe aus (ohne Zeilenumbruch nach jedem Element). Im Gegensatz dazu gibt map
für jedes Element einen neuen Wert zurück (und Seiteneffekte sollten vermieden werden). Der letzte Aufruf von println
gibt einen Zeilenumbruch aus, bevor das Programm beendet wird.
Das Konzept der Seiteneffekte bedeutet, dass die Funktion, die wir an foreach
übergeben, etwas tut, das den Zustand außerhalb des lokalen Kontexts beeinflusst. Wir könnten in eine Datenbank oder in eine Datei schreiben, auf der Konsole ausgeben oder Raketen starten...
Sieh dir noch einmal die zweite Zeile innerhalb jeder Methode an, wie prägnant sie ist, wenn wir Operationen zusammenfassen. Wie wir immer wieder sehen werden, können wir durch die Aneinanderreihung von Transformationen prägnante und leistungsstarke Programme erstellen.
Bisher mussten wir noch keine Bibliothekselemente importieren, aber Scala-Importe funktionieren ähnlich wie ähnliche Konstrukte in anderen Sprachen. Scala importiert automatisch viele häufig verwendete Typen und Objektmitglieder, wie Seq
, List
, Vector
und die von uns verwendeten print*
Methoden, die eigentlich Methoden in einem object
namens scala.Console
. Die meisten dieser Dinge, die automatisch importiert werden, sind in einem Bibliotheksobjekt namens Predef
.
Der Vollständigkeit halber wollen wir noch darauf eingehen, wie du das Beispiel außerhalb von sbt
kompilierst und ausführst. Zuerst verwendest du scalac
, um eine JVM-kompatible .class
Datei zu kompilieren. Oft werden dabei mehrere Klassendateien erzeugt. Dann benutzt du scala
, um sie auszuführen.
Wenn du die Scala-Befehlszeilen-Tools separat installiert hast (siehe "Befehlszeilen-Schnittstellen-Tools" ), führe die folgenden zwei Befehle (ohne die Eingabeaufforderung $
) in einem Terminalfenster im Stammverzeichnis des Projekts aus:
$
scalac src/main/scala/progscala3/introscala/UpperMain1.scala$
scala -classpath . progscala3.introscala.Hello Hello compiled World! Hello: HELLO COMPILED WORLD!
Du solltest nun neue Verzeichnisse progscala3/introscala mit mehreren .class- und.tasty-Dateien haben, darunter eine Datei namens UpperMain1.class
. Klassendateien werden von der JVM verarbeitet, und tasty-Dateien sind eine Zwischendarstellung, die vom Compiler verwendet wird. Scala muss gültigen JVM Bytecode und Dateien erzeugen. Zum Beispiel muss die Verzeichnisstruktur mit der Paketstruktur übereinstimmen. Die Option -classpath .
fügt das aktuelle Verzeichnis zum Suchklassenpfad hinzu, obwohl .
der Standard ist.
Wenn wir sbt
erlauben, sie für uns zu kompilieren, brauchen wir ein anderes -classpath
Argument, um das Verzeichnis anzugeben, in das sbt
die Klassendateien schreibt:
$
scala -classpath target/scala-3.0.0/classes progscala3.introscala.Hello Bye!
BYE!
Machen wir noch eine letzte Version, um ein paar andere nützliche Möglichkeiten für die Arbeit mit Sammlungen in diesem Szenario zu sehen. Dies ist die Version, die wir zuvor ausgeführt haben:
// src/main/scala/progscala3/introscala/UpperMain2.scala
package
progscala3
.
introscala
@main
def
Hello2
(
params
:
String
*
):
Unit
=
val
output
=
params
.
map
(
_
.
toUpperCase
).
mkString
(
" "
)
println
(
output
)
Anstatt wie bisher foreach
zu verwenden, um jede umgewandelte Zeichenkette zu drucken, bilden wir die Zeichenkettenfolge auf eine neue Zeichenkettenfolge ab und rufen dann eine Komfortmethode auf, mkString
auf, um die Zeichenketten zu einer endgültigen Zeichenkette zu verketten. Es gibt drei mkString
Methoden. Eine nimmt keine Argumente entgegen. Die zweite Version nimmt einen einzelnen Parameter an, um das Trennzeichen zwischen den Elementen anzugeben (in unserem Beispiel" "
). Die dritte Version nimmt drei Parameter entgegen: einen Präfix-String ganz links, das Trennzeichen und einen Suffix-String ganz rechts. Versuche, den Code so zu ändern, dass er mkString("[", ", ", "]")
verwendet.
Beachte die Funktion, die an map
übergeben wird. Die folgenden Funktionsliterale sind im Wesentlichen die gleichen:
s
=>
s
.
toUpperCase
_
.
toUpperCase
Anstatt einen Namen für das einzelne Argument anzugeben, können wir _
als Platzhalter verwenden. Dies gilt allgemein für Funktionen mit zwei oder mehr Argumenten, bei denen jede Verwendung von _
ein Argument ersetzt. Das bedeutet, dass Platzhalter nicht verwendet werden können, wenn es notwendig ist, auf eines der Argumente mehr als einmal zu verweisen.
Wie zuvor können wir diesen Code mit sbt
ausführen, indem wir runMain progscala3.introscala.Hello2
verwenden. Wir haben auch schon gesehen, dass wir den Befehl scala
verwenden können, um ihn in einem Schritt zu kompilieren und auszuführen, da er einen einzigen Einstiegspunkt hat:
$
scala src/main/scala/progscala3/introscala/UpperMain2.scala last Hello World!
LAST HELLO WORLD!
Ein Anwendungsbeispiel
Zum Abschluss dieses Kapitels wollen wir einige weitere verführerische Funktionen von Scala anhand einer Beispielanwendung erkunden. Wir verwenden eine vereinfachte Hierarchie geometrischer Formen, die wir an ein anderes Objekt senden, um sie auf einem Display zu zeichnen. Stell dir ein Szenario vor, in dem eine Spiel-Engine Szenen erzeugt. Wenn die Formen in der Szene fertiggestellt sind, werden sie zum Zeichnen an ein Display-Subsystem gesendet.
Zu Beginn definieren wir eine Shape
Klassenhierarchie:
// src/main/scala/progscala3/introscala/shapes/Shapes.scala
package
progscala3
.
introscala
.
shapes
case
class
Point
(
x
:
Double
=
0.0
,
y
:
Double
=
0.0
)
abstract
class
Shape
(
)
:
/*
*
*
Draw the shape.
*
@param f is a function to which the shape will pass a
*
string version of itself to be rendered.
*/
def
draw
(
f
:
String
=>
Unit
)
:
Unit
=
f
(
s"
draw:
$
this
"
)
case
class
Circle
(
center
:
Point
,
radius
:
Double
)
extends
Shape
case
class
Rectangle
(
lowerLeft
:
Point
,
height
:
Double
,
width
:
Double
)
extends
Shape
case
class
Triangle
(
point1
:
Point
,
point2
:
Point
,
point3
:
Point
)
extends
Shape
Deklariere eine Klasse für zweidimensionale Punkte. Es sind keine Mitglieder definiert, also lassen wir den Doppelpunkt (
:
) am Ende der Klassensignatur weg.Deklariere eine abstrakte Klasse für geometrische Formen. Sie braucht einen Doppelpunkt, weil sie eine Methode
draw
definiert.Implementiere eine
draw
Methode zum Rendern der Formen. Der Kommentar verwendet die Scaladoc-Konventionen für die Dokumentation der Methode, die den Javadoc-Konventionen ähnlich sind.Ein Kreis mit einem Mittelpunkt und einem Radius, der die Untertypen (
extends
)Shape
.Ein Rechteck mit einem linken unteren Punkt, einer Höhe und einer Breite. Der Einfachheit halber sind die Seiten parallel zu den horizontalen und vertikalen Achsen.
Ein Dreieck, das durch drei Punkte definiert ist.
Lass uns auspacken, was los ist.
Die Parameterliste nach dem Point
Klassennamen ist die Liste der Konstruktorparameter. In Scala ist der gesamte Körper einer class
oder object
der Konstruktor, also listest du die Parameter für den Konstruktor nach dem Klassennamen und vor dem Körper der Klasse auf.
Das Schlüsselwort case
vor der Klassendeklaration bewirkt eine besondere Behandlung. Zunächst wird jeder Konstruktorparameter automatisch in ein schreibgeschütztes (unveränderliches) Feld für Point
Instanzen umgewandelt. Mit anderen Worten: Es ist so, als ob wir val
vor jede Felddeklaration setzen würden. Wenn du eine Point
Instanz mit dem Namen point
instanziierst, kannst du die Felder mit point.x
und point.y
lesen, aber du kannst ihre Werte nicht ändern. Der Versuch, point.y = 3.0
zu schreiben, führt zu einem Kompilierungsfehler.
Du kannst auch Standardwerte für Konstruktor- und Methodenparameter angeben. Die = 0.0
nach jeder Parameterdefinition gibt 0.0
als Standardwert an. Der Benutzer muss sie also nicht explizit angeben, sondern sie werden von links nach rechts abgeleitet. Das bedeutet, dass du, wenn du einen Standardwert für einen Parameter definierst, dies auch für alle Parameter rechts davon tun musst.
Schließlich werden Instanzen von Fallklassen ohne new
konstruiert, z. B. val p = Point(
...)
. Scala 3 bietet die Möglichkeit, new
bei der Konstruktion von Instanzen für die meisten Nicht-Fallklassen wegzulassen. Bisher haben wir new Upper1()
verwendet, aber auch das Weglassen von new
würde funktionieren. Wir werden das von nun an tun, aber es gibt Situationen, in denen new
immer noch notwendig ist.
Verwenden wir sbt console
, um mit diesen Typen zu spielen. Ich empfehle dir, dies mit den meisten Beispielen in diesem Buch zu tun. Erinnere dich daran, dass scala>
die Eingabeaufforderung der scala
REPL ist. Wenn du eine Zeile siehst, die mit // src/script/
beginnt, ist sie nicht Teil der Sitzung, sondern zeigt dir, wo du diesen Code in der Beispieldistribution finden kannst.
$
sbt
>
console
...
// src/script/scala/progscala3/introscala/TryShapes.scala
scala
>
import
progscala3
.
introscala
.
shapes
.
*
scala
>
val
p00
=
Point
()
val
p00
:
progscala3
.
introscala
.
shapes
.
Point
=
Point
(
0.0
,
0.0
)
scala
>
val
p20
=
Point
(
2.0
)
val
p20
:
progscala3
.
introscala
.
shapes
.
Point
=
Point
(
2.0
,
0.0
)
scala
>
val
p20b
=
Point
(
2.0
)
val
p20b
:
progscala3
.
introscala
.
shapes
.
Point
=
Point
(
2.0
,
0.0
)
scala
>
val
p02
=
Point
(
y
=
2.0
)
val
p02
:
progscala3
.
introscala
.
shapes
.
Point
=
Point
(
0.0
,
2.0
)
scala
>
p20
==
p20b
val
res0
:
Boolean
=
true
scala
>
p20
==
p02
val
res1
:
Boolean
=
false
Wie in vielen anderen Sprachen wird bei Import-Anweisungen das Zeichen *
als Platzhalter verwendet, um alles aus dem Paket progscala3.introscala.shapes
zu importieren. Dies ist eine Änderung gegenüber Scala 2, wo _
als Platzhalter verwendet wurde. Aus Gründen der Abwärtskompatibilität ist dies jedoch noch bis zu einer zukünftigen Version von Scala 3 erlaubt. Wir haben gesehen, dass _
auch in Funktionsliteralen als anonymer Platzhalter für einen Parameter verwendet wurde, anstatt einen expliziten Namen zu verwenden.
In der Definition von p00
werden keine Argumente angegeben, also verwendet Scala 0.0
für beide. (Allerdings musst du die leeren Klammern angeben.) Wenn ein Argument angegeben wird, wendet Scala es auf das Argument ganz links an, x
, und verwendet den Standardwert für das verbleibende Argument, wie für p20
und p20b
gezeigt. Wir können die Argumente sogar mit Namen angeben. Die Definition von p02
verwendet den Standardwert für x
, gibt aber den Wert für y
an, indem sie Point(y = 2.0)
verwendet.
Tipp
Ich verwende oft benannte Argumente wie diese, auch wenn sie nicht erforderlich sind, denn Point(x = 0.0, y = 2.0)
macht meinen Code viel einfacher zu lesen und zu verstehen.
Für Point
gibt es zwar keinen Klassenkörper, aber ein weiteres Merkmal des Schlüsselworts case
ist, dass der Compiler automatisch mehrere Methoden für uns generiert, darunter die häufig verwendeten Methoden toString
, equals
und hashCode
. Die für jeden Punkt angezeigte Ausgabe - z. B. Point(2.0,0.0)
- ist die Standardausgabe von toString
. Die Methoden equals
und hashCode
sind für die meisten Entwickler nur schwer korrekt zu implementieren, daher ist die automatische Generierung dieser Methoden ein echter Vorteil. Du kannst aber auch eigene Definitionen für jede dieser Methoden erstellen, wenn du das möchtest.
Als wir fragten, ob p20 == p20b
und p20 == p02
, rief Scala die generierte Methode equals
auf, die die Instanzen auf Gleichheit vergleicht, indem sie die Felder vergleicht. (In einigen Sprachen vergleicht ==
nur Referenzen. Zeigen p20
und p20b
auf dieselbe Stelle im Speicher?)
Das letzte Merkmal der Fallklassen, das wir jetzt erwähnen wollen, ist, dass der Compiler für jede Fallklasse auch ein Begleitobjekt, ein Singleton-Objekt mit demselben Namen, erzeugt. Mit anderen Worten: Wir haben class Point
deklariert, und der Compiler hat auch ein object Point
erstellt.
Hinweis
Du kannst Begleitpersonen selbst definieren. Wenn ein object
und ein class
denselben Namen haben und in derselben Datei definiert sind, sind sie Gefährten.
Der Compiler fügt dem Companion-Objekt außerdem automatisch mehrere Methoden hinzu, von denen eine den Namen apply
trägt. Sie nimmt die gleiche Parameterliste wie der Konstruktor. Wenn ich vorhin gesagt habe, dass es nicht nötig ist, new
zu verwenden, um Instanzen von Fallklassen wie Point
zu erstellen, funktioniert das, weil die Begleitmethode Point.apply(
...)
aufgerufen wird.
Das gilt für jede Instanz, entweder eine deklarierte object
oder eine Instanz eines class
, nicht nur für die Begleitobjekte der Fallklasse. Wenn du eine Argumentliste dahinter setzt, sucht Scala nach einer entsprechenden apply
Methode, die du aufrufen kannst. Daher sind die folgenden beiden Zeilen gleichwertig:
val
p1
=
Point
.
apply
(
1.0
,
2.0
)
// Point is the companion object here!
val
p2
=
Point
(
1.0
,
2.0
)
// Same!
Es ist ein Kompilierungsfehler, wenn keine apply
Methode für die Instanz existiert oder die angegebene Argumentliste nicht mit dem kompatibel ist, was apply
erwartet.6
Die Methode Point.apply
ist quasi eine Fabrik für die Konstruktion von Points
. Das Verhalten ist einfach: Es ist wie der Aufruf des Konstruktors der Klasse Point
. Das erzeugte Begleitobjekt entspricht dem hier:
object
Point
:
def
apply
(
x
:
Double
=
0.0
,
y
:
Double
=
0.0
)
=
new
Point
(
x
,
y
)
.
.
.
Hier ist unser erstes Beispiel, in dem
new
noch gebraucht wird. Ohne sie würde der Compiler denken, dass wirPoint.apply
noch einmal auf der rechten Seite aufrufen und damit eine unendliche Rekursion erzeugen!
Du kannst dem Begleitobjekt Methoden hinzufügen, einschließlich überladener Methoden apply
. Deklariere einfach object Point:
explizit und füge die Methoden hinzu. Die Standardmethode apply
wird weiterhin generiert, es sei denn, du definierst sie ausdrücklich selbst.
Eine anspruchsvollere apply
Methode kann je nach Argument einen anderen Subtyp mit speziellem Verhalten instanziieren. Für eine Datenstruktur kann es zum Beispiel eine Implementierung geben, die für eine kleine Anzahl von Elementen optimal ist, und eine andere, die für eine größere Anzahl von Elementen optimal ist. Die Methode apply
kann diese Logik ausblenden und dem Benutzer eine einzige, vereinfachte Schnittstelle bieten. Daher ist es üblich, eine apply
Methode auf ein Begleitobjekt anzuwenden, um eine Fabrikmethode für eine Klassenhierarchie zu definieren, unabhängig davon, ob es sich um Fallklassen handelt oder nicht.
Wir können auch eine Instanz apply
Methode in jeder class
definieren. Sie hat die Bedeutung, die wir für Instanzen für angemessen halten. Zum Beispiel ruft Seq.apply(index: Int)
das Element an der Position index
ab, wobei von Null an gezählt wird.
Hinweis
Zur Erinnerung: Wenn eine Argumentliste hinter einer object
oder class
Instanz steht, sucht Scala nach einer apply
Methode, die aufgerufen werden kann, wenn die Parameterliste mit den angegebenen Argumenten übereinstimmt. Daher verhält sich alles mit einer apply
Methode wie eine Funktion -z.B. Point(2.0, 3.0)
.
Die Methode apply
eines Begleitobjekts ist eine Fabrikmethode für die Instanzen der Begleitklasse. Eine Methode der Klasse apply
hat die Bedeutung, die für Instanzen der Klasse angemessen ist; zum Beispiel gibt Seq.apply(index: Int)
das Element an der Position index
zurück.
Um bei dem Beispiel zu bleiben: Shape
ist eine abstrakte Klasse. Wir können eine abstrakte Klasse nicht instanziieren, auch wenn keines ihrer Mitglieder abstrakt ist. Shape.draw
ist definiert, aber wir wollen nur konkrete Formen instanziieren: Circle
, Rectangle
und Triangle
.
Der Parameter f
für draw
ist eine Funktion vom Typ String => Unit
. Wir haben Unit
bereits gesehen. Es ist ein echter Typ, aber er verhält sich ungefähr wie void
in anderen Sprachen.
Die Idee ist, dass der Aufrufer von draw
eine Funktion übergibt, die das eigentliche Zeichnen übernimmt, wenn er eine String-Repräsentation der Form erhält. Der Einfachheit halber verwenden wir nur den von toString
zurückgegebenen String, aber ein strukturiertes Format wie JSON wäre in einer echten Anwendung sinnvoller.
Tipp
Wenn eine Funktion Unit
zurückgibt, hat sie nur Nebeneffekte. Die Funktion gibt nichts Nützliches zurück und kann daher nur Nebeneffekte auf einen Zustand ausüben, wie z. B. eine Eingabe oder Ausgabe (I/O).
Normalerweise bevorzugen wir in FP reine Funktionen, die keine Seiteneffekte haben und ihre gesamte Arbeit als Rückgabewert zurückgeben. Diese Funktionen sind viel einfacher zu verstehen, zu testen, zusammenzustellen und wiederzuverwenden. Seiteneffekte sind eine häufige Fehlerquelle, deshalb sollten sie mit Bedacht eingesetzt werden.
Tipp
Verwende Seiteneffekte nur, wenn es nötig ist und an genau definierten Stellen. Halte den Rest des Codes rein.
Shape.draw
ist ein weiteres Beispiel, bei dem eine Funktion als Argument übergeben wird, genauso wie wir Instanzen von Strings
, Points
usw. übergeben können. Wir können auch Funktionen von Methoden und anderen Funktionen zurückgeben. Schließlich können wir Funktionen auch Variablen zuweisen. Das bedeutet, dass Funktionen in Scala erstklassig sind, weil sie genau wie Strings und andere Instanzen verwendet werden können. Dies ist ein mächtiges Werkzeug, um komponierbare und dennoch flexible Software zu entwickeln.
Wenn eine Funktion andere Funktionen als Parameter akzeptiert oder Funktionen als Werte zurückgibt, nennt man sie eine Funktion höherer Ordnung (HOF).
Man könnte sagen, dass draw
ein Protokoll definiert, das alle Shapes unterstützen müssen, das aber von den Benutzern angepasst werden kann. Jeder Shape muss seinen Zustand mit der Methode toString
in eine String-Darstellung serialisieren. Die Methode f
wird von draw
aufgerufen, die die endgültige Zeichenkette mit Hilfe einerinterpolierten Zeichenkette erstellt.
Ein interpolierter String beginnt mit s
vor dem öffnenden doppelten Anführungszeichen: s"draw: ${this.toString}"
. Sie bildet die endgültige Zeichenkette, indem sie das Ergebnis des Ausdrucks this.toString
in die größere Zeichenkette einfügt. Eigentlich brauchen wir toString
nicht aufzurufen; es wird für uns aufgerufen. Wir können also einfach ${this}
verwenden. Jetzt beziehen wir uns aber nur auf eine Variable und nicht auf einen längeren Ausdruck, also können wir die geschweiften Klammern weglassen und einfach $this
schreiben. Die interpolierte Zeichenkette wird also zu s"draw: $this"
.
Warnung
Wenn du das s
vor der interpolierten Zeichenkette vergisst, erhältst du die wörtliche Ausgabe draw: $this
, ohne Interpolation.
Um beim Beispiel zu bleiben: Circle
, Rectangle
und Triangle
sind konkrete Subtypen (auch Unterklassen genannt) von Shape
. Sie haben keine Klassenkörper, weil Shape
und die für case
generierten Methoden alle Methoden definieren, die wir brauchen, wie z.B. die toString
Methoden, die von Shape.draw
benötigt werden.
In unserem einfachen Programm wird die f
, die wir an draw
übergeben, nur den String in die Konsole schreiben. In einer echten Anwendung könnte f
den String parsen und die Form auf einem Display darstellen, JSON an einen Webservice schreiben usw.
Auch wenn es sich um eine Single-Thread-Anwendung handelt, wollen wir vorwegnehmen, was wir in einer gleichzeitigen Implementierung tun könnten, indem wir eine Reihe möglicher Message
s definieren, die zwischen den Modulen ausgetauscht werden können:
// src/main/scala/progscala3/introscala/shapes/Messages.scala
package
progscala3
.
introscala
.
shapes
sealed
trait
Message
case
class
Draw
(
shape
:
Shape
)
extends
Message
case
class
Response
(
message
:
String
)
extends
Message
case
object
Exit
extends
Message
Deklariere eine
trait
mit dem NamenMessage
. Einetrait
ist ähnlich wie eine abstrakte Basisklasse. (Auf die Unterschiede gehen wir später ein.) Alle ausgetauschten Nachrichten sind Untertypen vonMessage
. Das Schlüsselwortsealed
erkläre ich gleich.Eine Nachricht, um die beigefügte
Shape
zu zeichnen.Eine Nachricht mit einer Antwort auf eine vorhergehende Nachricht, die von einem Anrufer empfangen wurde.
Signal Beendigung.
Exit
hat keinen eigenen Zustand und kein eigenes Verhalten, deshalb wird es alscase object
deklariert, da wir nur eine Instanz davon brauchen. Es fungiert als Signal, um eine Zustandsänderung auszulösen, in diesem Fall die Beendigung.
Das Schlüsselwort sealed
bedeutet, dass wir nur Subtypen von Message
in derselben Datei definieren können. Das verhindert Fehler, bei denen Benutzer ihre eigenen Message
Untertypen definieren, die den Code, den wir in der nächsten Datei sehen werden, kaputt machen würden! Dies sind alle erlaubten Nachrichten, die im Voraus bekannt sind.
Erinnere dich daran, dass Shape
nicht als sealed
deklariert wurde, weil wir beabsichtigen, dass die Leute ihre eigenen Subtypen davon erstellen können. Im Prinzip könnte es eine unendliche Anzahl von Shape
Untertypen geben. Verwende also versiegelte Hierarchien, wenn alle möglichen Varianten festgelegt sind.
Nachdem wir nun unsere Formen und Nachrichtentypen definiert haben, lass uns eine object
für die Verarbeitung von Nachrichten definieren:
// src/main/scala/progscala3/introscala/shapes/ProcessMessages.scala
package
progscala3
.
introscala
.
shapes
object
ProcessMessages
:
def
apply
(
message
:
Message
)
:
Message
=
message
match
case
Exit
=>
println
(
s"
ProcessMessage: exiting...
"
)
Exit
case
Draw
(
shape
)
=>
shape
.
draw
(
str
=>
println
(
s"
ProcessMessage:
$
str
"
)
)
Response
(
s"
ProcessMessage:
$
shape
drawn
"
)
case
Response
(
unexpected
)
=>
val
response
=
Response
(
s"
ERROR: Unexpected Response:
$
unexpected
"
)
println
(
s"
ProcessMessage:
$
response
"
)
response
Wir brauchen nur eine Instanz, also verwenden wir
object
, aber es wäre einfach genug, daraus eineclass
zu machen und so viele zu instanziieren, wie wir für die Skalierbarkeit und andere Bedürfnisse brauchen.Definiere die Methode
apply
, die eineMessage
annimmt, sie verarbeitet und dann eine neueMessage
zurückgibt.Vergleiche die eingehende Nachricht, um zu entscheiden, was mit ihr geschehen soll.
Die Methode apply
führt einen leistungsstarken Funktionsaufruf ein: Match-Ausdrücke mit Mustervergleich:
message
match
case
Exit
=>
expressions
case
Draw
(
shape
)
=>
expressions
case
Response
(
unexpected
)
=>
expressions
Der gesamte message match:
... ist ein Ausdruck, das heißt, er gibt einen Wert zurück, einen neuen Message
, den wir an den Aufrufer zurückgeben. Ein match
Ausdruck besteht nur aus case
Klauseln, die einen Mustervergleich mit der an die Funktion übergebenen Nachricht durchführen, gefolgt von Ausdrücken, die bei einer Übereinstimmung aufgerufen werden.
Die match
Ausdrücke funktionieren ähnlich wie die if/else
Ausdrücke, sind aber mächtiger und prägnanter. Wenn eines der Muster übereinstimmt, wird der Block von Ausdrücken nach dem Pfeil (=>
) ausgewertet, bis zum nächsten case
Schlüsselwort oder dem Ende des gesamten Ausdrucks. Das Matching ist eifrig; die erste Übereinstimmung gewinnt.
Wenn die Fallklauseln nicht alle möglichen Werte abdecken, die an den match
Ausdruck übergeben werden können, wird ein MatchError
zur Laufzeit ausgelöst. Glücklicherweise kann der Compiler erkennen und dich warnen, dass die Fallklauseln nicht erschöpfend sind, d. h. nicht alle möglichen Eingaben abdecken. Beachte, dass unsere sealed
Hierarchie der Meldungen hier entscheidend ist. Wenn ein Benutzer einen neuen Untertyp von Message
erstellen könnte, würde unser match
Ausdruck nicht mehr alle Möglichkeiten abdecken. Dadurch würde ein Fehler in diesem Code entstehen!
Eine leistungsstarke Funktion des Mustervergleichs ist die Möglichkeit, Daten aus dem übereinstimmenden Objekt zu extrahieren, was manchmal als Dekonstruktion (das Gegenteil von Konstruktion) bezeichnet wird. Wenn die Eingabe message
eine Draw
ist, extrahieren wir die eingeschlossene Shape
und weisen sie der Variablen shape
zu. Ähnlich verhält es sich, wenn Response
erkannt wird: Wir extrahieren die Nachricht als unexpected
, so genannt, weil ProcessMessages
nicht erwartet, eine Response
zu erhalten!
Schauen wir uns nun die Ausdrücke an, die für jede Fallübereinstimmung aufgerufen werden:
def
apply
(
message
:
Message
)
:
Message
=
message
match
case
Exit
=>
println
(
s"
ProcessMessage: exiting...
"
)
Exit
case
Draw
(
shape
)
=>
shape
.
draw
(
str
=>
println
(
s"
ProcessMessage:
$
str
"
)
)
Response
(
s"
ProcessMessage:
$
shape
drawn
"
)
case
Response
(
unexpected
)
=>
val
response
=
Response
(
s"
ERROR: Unexpected Response:
$
unexpected
"
)
println
(
s"
ProcessMessage:
$
response
"
)
response
Wir sind fertig, also geben wir eine Nachricht aus, dass wir uns beenden und
Exit
an den Aufrufer zurückgeben.Rufe
draw
aufshape
auf und übergebe ihm eine anonyme Funktion, die weiß, was sie mit der vondraw
erzeugten Zeichenkette machen soll. In diesem Fall gibt sie die Zeichenkette einfach auf der Konsole aus und sendet eineResponse
an den Aufrufer.ProcessMessages
erwartet nicht, eineResponse
Nachricht vom Aufrufer zu erhalten, und behandelt sie daher als Fehler. Er gibt eine neueResponse
an den Aufrufer zurück.
Einer der Grundsätze der OOP ist, dass du niemals if
oder match
Anweisungen verwenden solltest, die mit einem Instanztyp übereinstimmen, da sich Vererbungshierarchien weiterentwickeln. Wenn ein neuer Untertyp eingeführt wird, ohne dass diese Anweisungen ebenfalls angepasst werden, gehen sie kaputt. Stattdessen sollten polymorphe Methoden verwendet werden. Ist der gerade besprochene Pattern-Matching-Code also ein Antipattern?
Unser match
Ausdruck kennt nur Shape
und draw
. Wir suchen nicht nach bestimmten Untertypen von Shape
. Das bedeutet, dass unser Code nicht abbricht, wenn ein Benutzer einen neuen Shape
zur Hierarchie hinzufügt.
Im Gegensatz dazu passen die Fallklauseln auf bestimmte Untertypen von Message
, aber wir haben uns vor unerwarteten Änderungen geschützt, indem wir Message
zu einer sealed
Hierarchie gemacht haben. Wir kennen alle möglichen Message
s, die ausgetauscht werden können.
Wir haben also das polymorphe Dispatch aus der OOP mit dem Pattern Matching, einem Arbeitspferd der FP, kombiniert. Dies ist eine Möglichkeit, wie Scala diese beiden Programmierparadigmen elegant integriert!
Hier ist die ProcessShapesDriver
, die das Beispiel ausführt:
// src/main/scala/progscala3/introscala/shapes/ProcessShapesDriver.scala
package
progscala3
.
introscala
.
shapes
@main
def
ProcessShapesDriver
=
val
messages
=
Seq
(
Draw
(
Circle
(
Point
(
0.0
,
0.0
)
,
1.0
)
)
,
Draw
(
Rectangle
(
Point
(
0.0
,
0.0
)
,
2
,
5
)
)
,
Response
(
s"
Say hello to pi: 3.14159
"
)
,
Draw
(
Triangle
(
Point
(
0.0
,
0.0
)
,
Point
(
2.0
,
0.0
)
,
Point
(
1.0
,
2.0
)
)
)
,
Exit
)
messages
.
foreach
{
message
=>
val
response
=
ProcessMessages
(
message
)
println
(
response
)
}
Ein Einstiegspunkt für die Anwendung. Er nimmt keine Argumente entgegen. Wenn du beim Ausführen der Anwendung Argumente angibst, werden sie ignoriert.
Eine Folge von zu sendenden Nachrichten, einschließlich einer
Response
in der Mitte, die inProcessMessages
als Fehler betrachtet wird. Die Sequenz endet mitExit
.Gehe durch die Abfolge der Nachrichten, rufe mit jeder Nachricht
ProcessMessages.apply()
auf und drucke dann die Antwort aus.
Probieren wir es aus. Einige Ausgaben wurden ausgelassen:
>
runMain
progscala3
.
introscala
.
shapes
.
ProcessShapesDriver
[
info
]
running
progscala3
.
introscala
.
shapes
.
ProcessShapesDriver
ProcessMessage
:
draw
:
Circle
(
Point
(
0.0
,
0.0
),
1.0
)
Response
(
ProcessMessage
:
Circle
(
Point
(
0.0
,
0.0
),
1.0
)
drawn
)
ProcessMessage
:
draw
:
Rectangle
(
Point
(
0.0
,
0.0
),
2.0
,
5.0
)
Response
(
ProcessMessage
:
Rectangle
(
Point
(
0.0
,
0.0
),
2.0
,
5.0
)
drawn
)
ProcessMessage
:
Response
(
ERROR
:
Unexpected
Response
:
Say
hello
to
pi
:
3.14159
)
Response
(
ERROR
:
Unexpected
Response
:
Say
hello
to
pi
:
3.14159
)
ProcessMessage
:
draw
:
Triangle
(
Point
(
0.0
,
0.0
),
Point
(
2.0
,
0.0
),
Point
(
1.0
,
2.0
))
Response
(
ProcessMessage
:
Triangle
(
Point
(
0.0
,
0.0
),
...)
drawn
)
ProcessMessage
:
exiting
...
Exit
[
success
]
...
Vergewissere dich, dass du verstehst, wie jede Nachricht verarbeitet wurde und woher jede Zeile der Ausgabe stammt.
Rekapitulation und was kommt als Nächstes?
Wir haben viele der leistungsstarken und übersichtlichen Funktionen von Scala vorgestellt. Wenn du Scala erkundest, wirst du weitere nützliche Ressourcen finden. Du findest Links zu Bibliotheken, Tutorien und verschiedenen Artikeln, die die Funktionen der Sprache beschreiben.
Als Nächstes setzen wir unsere Einführung in die Funktionen von Scala fort und betonen die verschiedenen prägnanten und effizienten Möglichkeiten, viel Arbeit zu erledigen.
1 Wenn du mit dem JVM-Ökosystem nicht vertraut bist, ist der Klassenpfad eine Liste von Orten, an denen nach kompiliertem Code gesucht wird, z. B. Bibliotheken.
2 Manchmal werden die Typinformationen als Annotation bezeichnet, aber das kann zu Verwechslungen mit einem anderen Konzept von Annotationen führen, das wir noch sehen werden, daher werde ich diesen Begriff für Typen nicht verwenden. Typenzuschreibungen ist ein anderer Begriff.
3 Ich werde die Codebeispiele regelmäßig aktualisieren, wenn neue Scala-Versionen herauskommen. Die Version wird in der Datei build.sbt, dem scalaVersion
String, festgelegt. Die andere Möglichkeit, die Version festzustellen, ist, sich den Inhalt des Verzeichnisses target
anzusehen.
4 Diese Methode benötigt keine Argumente, daher können die Klammern weggelassen werden.
5 Diese Formatierung im Stil von printf
ist in Programmiersprachen so üblich, dass sie wohl keiner weiteren Erklärung bedarf. Wenn sie für dich neu ist, findest du unter dem Link in diesem Absatz weitere Informationen.
6 Der Name apply
geht auf frühe theoretische Arbeiten zum Rechnen zurück, insbesondere auf die Idee der Funktionsanwendung.
Get Scala programmieren, 3. Auflage now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.