In diesem Artikel möchte ich die völlig willkürlich ausgewählten Top 5 der Neuerungen vorstellen. Diese und alle weiteren Änderungen sind ausführlich auf der Dotty-Webseite dokumentiert.
Platz 5: Opake Typen
Wer kennt das Problem nicht?
Man muss im Code physikalische Größen verwalten – zum Beispiel Zeiten – und verwechselt die Einheiten.
Durch welche Zehnerpotenz muss man System.nanoTime()
dividieren, um auf Sekunden zu kommen?
Typischerweise sind Größen im Code als Int
oder Double
repräsentiert, so dass der Compiler kaum Hilfestellung geben kann, wenn man sich verrechnet.
Dieses Problem ist in der Praxis derart häufig, dass sie teilweise sogar in rigoros analysiertem Code nicht aufgespürt werden.
In vielen funktionalen Programmiersprachen steht eine elegante Lösung dafür zur Verfügung:
man definiert kurzerhand einen „Wrapper“-Typen, der selbst zwar auch nur einen Double
enthält, aber explizite Konvertierungsmethoden anbietet.
In Scala 2 kann das zum Beispiel so aussehen:
So ähnlich wurde die Modellierung von Zeiträumen auch in der Standardbibliothek für Timeouts von asynchronen Operationen umgesetzt.
Während Scala hier die Vorteile ihrer konzisen Syntax gegenüber Java voll ausspielen kann, ist diese Art der Programmierung der Performance oft nicht zuträglich.
Denn müssen viele solcher Objekte erzeugt werden, wird ständig geboxt: ein Array[Seconds]
hat benötigt viel mehr Speicher als ein Array[Int]
, denn bekanntlich macht Kleinvieh auch Mist.
Scala 2 hat sogenannte „Value Classes“ eingeführt, die aber das Boxing-Problem in bestimmten Situationen nicht lösen konnten.
Hier setzt jetzt Scala 3 mit den sogenannten „Opaque Type Aliases“ an. Innerhalb eines Objekts lassen sich mehrere Synonyme definieren, einschließlich Methoden zur sicheren Konvertierung:
Das besondere hieran ist, dass die Identität Milliseconds = Int
nur innerhalb des Objekt-Scopes gegeben ist, so dass man die Konvertierungsmethoden direkt auf Int
implementieren kann.
Außerhalb des Scopes kann man aber nur auf die vorgegebenen Methoden zurückgreifen.
Im Gegensatz zu den alten Value Classes sind folglich die Implementierungsdetails standardmäßig versteckt.
So ist es im obigen Beispiel nicht möglich, ein Milliseconds
-Wert ohne Umweg über Seconds
zu konstruieren.
Stattdessen geht man über Sekunden:
Dabei sind die Opaque Type Aliases nicht bloß syntaktischer Zucker; sondern der Compiler garantiert auch, dass sie zur Laufzeit nicht existieren. Es wird also weder das JAR-File noch der Heap aufgebläht, wenn man großzügig Aliase benutzt.
Besonders interessant sind die Aliase also für Domain-driven Design, denn damit lassen sich fachliche Typen wie Adressen, Postleitzahlen oder Namen so modellieren, dass eine Verwechslung von vornherein ausgeschlossen wird.
Platz 4: Aufgeräumte implicits
Der Codeschnipsel vom vorherigen Platz enthält übrigens noch ein weiteres interessantes Feature von Scala 3: die „Extension Methods“. Diese sind eine der beiden Resultate des Frühjahrsputzes bei den Implicits.
Wollte man früher einer Klasse eine zusätzliche Methode spendieren, so musste man den Umweg über eine separate Klasse und einer zusätzlichen impliziten Konvertierung gehen:
Diese lästige Zeremonie wurde zuletzt durch die Einführung von Implicit Classes deutlich vereinfacht:
Scala 3 setzt auch hier die Axt an und beseitigt den Zwang zur eigentlich überflüssigen Klasse CircleOps
:
Man beachte, dass diese Methode direkt im Top Level einer Scala-Datei auftauchen kann und nicht in ein Objekt geschachtelt sein muss. Für Scala-Erfahrene wirkt die Syntax zunächst etwas befremdlich, aber erfahrungsgemäß gewöhnt man sich schnell daran, zumal sie auch mit der neuen Whitespace-Syntax zusammenspielt (nächster Platz).
Eine weitere Neuerung setzt bei den berühmt-berüchtigten Implicit Arguments an.
Viele Bibliotheken wie Cats nutzen diese, um existierenden Typen neue Fähigkeiten zu verleihen.
Betrachten wir zum Beispiel eine combine
-Operation, die Bestandteil der Typklasse Semigroup
ist:
Mit dieser praktischen Typklasse lässt sich die „Summe“ der Elemente einer Liste auch für andere Typen als Zahlen berechnen.
Dazu muss man zunächst das Semigroup
-Trait für existierende Typen implementieren:
Auch diese Deklaration kann man direkt in einer Datei hinschreiben, ohne sie in ein Objekt zu kapseln. Nun kann man z.B. Listen von Strings summieren:
Weitere Typen, zum Beispiel fachliche Klassen, lassen sich mit einer weiteren given
-Deklaration hinzufügen.
Allerdings ist der Aufruf noch etwas zu umständlich.
In Kombination mit den Extension Methods lässt sich das noch zusammendampfen:
Jetzt geht es kompakter:
Durch das Zusammenspiel dieser zwei Features erreicht Scala 3 eine enorm hohe Ausdrucksstärke, die aber weiterhin beherrschbar bleibt.
Platz 3: Optionale Klammern
Wer bei der Version „Scala 3“ direkt an „Python 3“ denkt, liegt damit gar nicht falsch. Denn nicht nur gibt es einige Änderungen, die nicht rückwärts-kompatibel sind, sondern es wurde auch eine alternative Syntax basierend auf Einrückungen eingeführt.
Das obige Beispiel lässt sich damit wie folgt umschreiben:
Der Compiler rekonstruiert anhand der Einrückung die Blockstruktur des Quelltextes.
Es ist allerdings weiterhin möglich, geschweifte Klammern hierfür zu benutzen.
Das Risiko, dass beide Stile in einer Codebasis gemischt werden und dadurch Chaos herrscht, besteht durchaus.
Im Zuge dessen hat das Dotty-Team die Möglichkeit vorgesehen, den Code automatisch vom Compiler automatisch umschreiben zu lassen.
Dazu braucht man lediglich die Optionen -rewrite -indent
zu übergeben.
Doch nicht nur die geschweiften Klammern lassen sich einsparen, auch bei einigen runden Klammern wurde der Rotstift angesetzt. Statt wie bisher bei Kontrollstrukturen die Bedingungen einzuklammern, kann man diese in Scala 3 wie folgt schreiben:
Diese neue Syntax für Kontrollstrukturen lag bereits seit 2011 in der Schublade und stieß damals auf wenig Gegenliebe. Im Zuge der Entwicklung von Dotty wurde sie dann neu aufgelegt.
Gemäß des Wadler’schen Gesetzes über das Design von Programmiersprachen wurden diese syntaktischen Änderungen von Scala 3 am heißesten diskutiert. Meiner subjektiven Meinung nach gewöhnt man sich schnell daran und ich persönlich möchte sie nicht mehr missen.
Platz 2: Metaprogrammierung
Beim Begriff „Metaprogrammierung“ werden viele aufhorchen. Gibt es das überhaupt noch? Möchte man das überhaupt noch? Tatsächlich erfreuen sich Makros in Scala großer Beliebtheit, wobei man fairerweise noch dazu sagen sollte, dass es sich dabei um Compile-Time-Metaprogrammierung handelt. Das heißt, Makros werden entweder als Annotation oder als „gewöhnlicher“ Funktionsaufruf getarnt, aber direkt vom Compiler verarbeitet, so dass zur Laufzeit keine Rückstände der Metaprogrammierung übrig bleiben.
Seit Scala in der Version 2.10 die Makros eingeführt hat, kam es zu einer regelrechten kambrischen Explosion der Anwendungsfälle.
Ein Dauerbrenner in modernen Applikationen ist die Serialisierung von Objekten nach JSON.
Gängige Java-Frameworks arbeiten hier mit Reflection zur Laufzeit, was äußerst fehleranfällig ist.
In Scala benutzt man stattdessen eine Technik, die unter dem Begriff „Derivation“ bekannt geworden ist.
Beispielhaft kann man mit der circe
-Bibliothek folgenden Code schreiben:
Die Funktion deriveEncoder
ist als Makro implementiert.
Sofern die JSON-Kodierung nicht möglich ist (weil z.B. die Kodierung eines Unterobjekts nicht bekannt ist), bricht der Compiler mit einer Fehlermeldung ab.
Unglücklicherweise fordert die Implementierung dieses Makros einiges an Expertinnenwissen ab. Dotty vereinfacht diese Konstruktion deutlich; es ist nunmehr nur noch Code nötig, der „weiß“, wie man die Encoder der jeweiligen Unterobjekte zusammensteckt, um einen Encoder für das ganze Objekt zu erhalten.
Aber auch weniger spezifische Anwendungsfälle sind einfacher geworden.
Dank verallgemeinerter inline
-Definitionen kann man z.B. die obige Fakultätsfunktion direkt vom Compiler ausrechnen lassen:
Doch Obacht: naive Implementierungen wie diese können die Compile-Zeiten drastisch erhöhen. Man sollte sie also sparsam einsetzen.
Platz 1: Enums
Lange wurde Scala dafür gescholten, dass eines der grundlegendsten Features von Java nicht angeboten wird: die Enumerations. Will man in Scala 2 z.B. eine Aufzählung für Farben definieren, hat man prinzipiell drei Möglichkeiten.
Zunächst gibt es eine eingebaute Enumeration
-Klasse in der Standardbibliothek:
Dummerweise sind solche Aufzählungen nicht gut gegen nachträgliche Erweiterung geschützt. Außerdem hilft einem der Compiler nicht, wenn man in einem Match-Ausdruck Fälle vergessen hat:
Wegen dieser Einschränkungen wurde diese Klasse nur selten eingesetzt.
Alternativ kann man sich mit einer Struktur aus einem sealed trait
und case object
s behelfen:
Diese Darstellung erfüllt die gewünschten Eigenschaften, ist aber geschwätzig.
Als dritte Möglichkeit kann man zusätzlich eine externe Bibliothek nutzen, die zwar den Boilerplate-Code nicht reduziert, aber noch einige nützliche Helferlein bietet, zum Beispiel die Methode withName
, die für einen String das passende Aufzählungsobjekt mit diesem Namen zurückliefert.
Scala 3 räumt mit diesem Wildwuchs auf und bietet eine standardisierte, kompakte Notation an:
Praktischerweise ist diese Notation äquivalent zum obigen Code-Schnipsel mit sealed trait
und case object
und bietet daher die gleichen Eigenschaften an, wie z.B. die sichere Verwendung in einem Match-Ausdruck.
Die praktischen Helferlein gibt es als Dreingabe vom Scala-Compiler, ohne dass man dafür weitere Bibliotheken bräuchte.
Man kann sich jetzt zurecht fragen, warum dieses „harmlose“ Feature in meiner Liste auf dem ersten Platz ist. Das liegt daran, dass – wie so oft in Scala – eine Vorlage aus Java gnadenlos aufgebohrt worden ist. Betrachten wir zum Beispiel das Datenmodell für eine Zeichenapplikation, bei der wir verschiedene Pinseltypen benutzen möchten:
Jeder Fall kann ein oder mehrere Attribute besitzen. Das gemeinsame Attribut (Größe des Pinsels) kann wie hier in eine separate Klasse ausgelagert werden, oder alternativ in jedem Einzelfall gelistet sein.
Gerade diese praktische Schreibweise wurde von der Scala-Community seit langer Zeit erwartet und wird meiner Einschätzung nach als erstes in den Codebasen Einzug halten.