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).

  • sbtdas 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 kompiliert sbt 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.

Hinweis

Da ich die Option "aggressiv" -source:future verwende, wirst du bei der Verwendung von sbt console Warnungen sehen, die in anderen Scala 3-Projekten, die diese Einstellung nicht verwenden, nicht auftreten.

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
:quit                    exit the REPL
:type <expression>       evaluate the type of the given expression
:doc <expression>        print the documentation for the given expression
:imports                 show import history
:reset                   reset the REPL to its initial state, ...

scala> val s = "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.01s
  + The != operator is implemented with the equals method 0.001s
  ...
[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@7673711e=

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                                   1

object UpperMain1:
  def main(params: Array[String]): Unit =                       2
    print("UpperMain1.main: ")
    params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
    println("")

def main(params: Array[String]): Unit =                         3
  print("main: ")
  params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
  println("")

@main def Hello(params: String*): Unit =                        4
  print("Hello: ")
  params.map(s => s.toUpperCase).foreach(s => printf("%s ",s))
  println("")
1

Deklariere den Ort des Pakets, progscala3.introscala.

2

Deklariere eine main Methode, einen Programmeinstiegspunkt, innerhalb einer object. Ich werde gleich erklären, was eine object ist.

3

Deklariere einen alternativen main Einstiegspunkt als Top-Level-Methode, die außerhalb von object liegt, aber auf das aktuelle Paket, progscala3.introscala, beschränkt ist.

4

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 Arrayin 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 Strings (so genannte wiederholte Parameter), die an den Methodenkörper übergeben werden, wo params mit einem unveränderlichen Seq[String] implementiert wird. Veränderbare Arrays 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, mkStringauf, 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.Hello2verwenden. 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)                    1

abstract class Shape():                                               2
  /**
   * 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")               3

case class Circle(center: Point, radius: Double) extends Shape        4

case class Rectangle(lowerLeft: Point, height: Double, width: Double) 5
      extends Shape

case class Triangle(point1: Point, point2: Point, point3: Point)      6
      extends Shape
1

Deklariere eine Klasse für zweidimensionale Punkte. Es sind keine Mitglieder definiert, also lassen wir den Doppelpunkt (:) am Ende der Klassensignatur weg.

2

Deklariere eine abstrakte Klasse für geometrische Formen. Sie braucht einen Doppelpunkt, weil sie eine Methode draw definiert.

3

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.

4

Ein Kreis mit einem Mittelpunkt und einem Radius, der die Untertypen (extends) Shape.

5

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.

6

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)      1
  ...
1

Hier ist unser erstes Beispiel, in dem new noch gebraucht wird. Ohne sie würde der Compiler denken, dass wir Point.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 Messages definieren, die zwischen den Modulen ausgetauscht werden können:

// src/main/scala/progscala3/introscala/shapes/Messages.scala
package progscala3.introscala.shapes

sealed trait Message                                                 1
case class Draw(shape: Shape) extends Message                        2
case class Response(message: String) extends Message                 3
case object Exit extends Message                                     4
1

Deklariere eine trait mit dem Namen Message. Eine trait ist ähnlich wie eine abstrakte Basisklasse. (Auf die Unterschiede gehen wir später ein.) Alle ausgetauschten Nachrichten sind Untertypen von Message. Das Schlüsselwort sealed erkläre ich gleich.

2

Eine Nachricht, um die beigefügte Shape zu zeichnen.

3

Eine Nachricht mit einer Antwort auf eine vorhergehende Nachricht, die von einem Anrufer empfangen wurde.

4

Signal Beendigung. Exit hat keinen eigenen Zustand und kein eigenes Verhalten, deshalb wird es als case 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:                                              1
  def apply(message: Message): Message =                             2
    message match                                                    3
      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
1

Wir brauchen nur eine Instanz, also verwenden wir object, aber es wäre einfach genug, daraus eine class zu machen und so viele zu instanziieren, wie wir für die Skalierbarkeit und andere Bedürfnisse brauchen.

2

Definiere die Methode apply, die eine Message annimmt, sie verarbeitet und dann eine neue Message zurückgibt.

3

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 =>                                                     1
      println(s"ProcessMessage: exiting...")
      Exit
    case Draw(shape) =>                                              2
      shape.draw(str => println(s"ProcessMessage: $str"))
      Response(s"ProcessMessage: $shape drawn")
    case Response(unexpected) =>                                     3
      val response = Response(s"ERROR: Unexpected Response: $unexpected")
      println(s"ProcessMessage: $response")
      response
1

Wir sind fertig, also geben wir eine Nachricht aus, dass wir uns beenden und Exit an den Aufrufer zurückgeben.

2

Rufe draw auf shape auf und übergebe ihm eine anonyme Funktion, die weiß, was sie mit der von draw erzeugten Zeichenkette machen soll. In diesem Fall gibt sie die Zeichenkette einfach auf der Konsole aus und sendet eine Response an den Aufrufer.

3

ProcessMessages erwartet nicht, eine Response Nachricht vom Aufrufer zu erhalten, und behandelt sie daher als Fehler. Er gibt eine neue Response 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 Messages, 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 =                                      1
  val messages = Seq(                                                2
    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 =>                                      3
    val response = ProcessMessages(message)
    println(response)
  }
1

Ein Einstiegspunkt für die Anwendung. Er nimmt keine Argumente entgegen. Wenn du beim Ausführen der Anwendung Argumente angibst, werden sie ignoriert.

2

Eine Folge von zu sendenden Nachrichten, einschließlich einer Response in der Mitte, die in ProcessMessages als Fehler betrachtet wird. Die Sequenz endet mit Exit.

3

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.