Nach 8 Jahren, 28000 Commits und 7400 Pull-Requests war es am 14. Mai 2021 endlich so weit: Scala 3 wurde veröffentlicht. Neben dem neuen Compiler „Dotty“ haben es eine neue Syntax sowie einige Neuerungen an der Sprache in Scala 3 geschafft. In dieser Blogpost-Reihe werden einige der interessanten Neuerungen im Detail diskutiert. In diesem ersten Teil der Reihe wird der „quiet mode“ vorgestellt. Dabei handelt es sich um eine alternative Syntax, die mit Einrückungen Blockbildung vornimmt, statt mit geschweiften Klammern.

Voraussetzungen

Zwar werden Neuerungen und Änderungen in der Sprache von Grund auf erläutert und Vergleiche zu Scala 2 ausführlich erklärt, dennoch unterstützt die Kenntnis von Scala 2 das Verständnis dieses Blogposts.

Weniger Klammern aber strikte Einrückung

Man kennt es bereits von Python oder Haskell: Code, der explizite Einrückung fordert, welche Auswirkungen auf die Gültigkeit und Bedeutung haben. Die einen lieben diesen Ansatz, da er visuell gleichmäßigeren und rauschärmeren Code zur Folge hat. Andere vermissen die Freiheiten, die sie durch explizite Klammerung erhalten und verlieren sich beim Zählen tiefer Einrückungen.

Es ist nicht verwunderlich, dass die neue Syntax in Scala 3 bei ihrer Konzeption und Einführung kontrovers diskutiert wurde. So hat etwa eine Diskussion für Feedback zu dieser Neuerung 579 Beiträge und es gibt dort hitzige Diskussionen zu entdecken.

Scala 3 setzt diesem Feature noch die Krone auf und macht es optional. Das bedeutet nicht nur, dass die (geschweifte) klammerfreie Syntax an- und ausgeschaltet werden kann. Beide Syntaxvarianten können parallel verwendet und miteinander vermischt werden. Dass hier die Befürchtung aufkommt, dass eine fragmentierte Syntaxlandschaft entsteht, ist durchaus nachvollziehbar.

Geschweifte Klammern sind optional

Betrachten wir die folgende kleine Scala-Klasse und deren Companion-Object.

case class Bottle(content: String, volume: Float, currentVolume: Float){

  def fill(additionalVolume: Float) : Bottle = {
    val maybeNewVolume = currentVolume + additionalVolume 
    if(maybeNewVolume > volume){
      this.copy(currentVolume = volume) 
    } else {
      this.copy(currentVolume = maybeNewVolume) 
    }
  }

  def empty() : Bottle = {
    this.copy(currentVolume = 0f)
  }

  def isEmpty() : Boolean = {
    this.volume == 0f
  }
}


object Bottle{
   def emptyMany(bottles : List[Bottle]) : List[Bottle] = {
     bottles.map{ x => x.empty() }
   }

   def fillMany(bottles : List[Bottle], volume: Float) : List[Bottle] = {
     for{
       bottle <- bottles
     } yield bottle.fill(volume))
   }
}

Diese Syntax kompiliert sowohl für Scala 2 als auch für Scala 3. In Scala 3 können wir die Klasse mit der neuen Syntaxvariante auch wie folgt beschreiben:

case class Bottle(content: String, volume: Float, currentVolume: Float):

  def fill(additionalVolume: Float) : Bottle = 
    val maybeNewVolume = currentVolume + additionalVolume 
    if maybeNewVolume > volume then
      this.copy(currentVolume = volume) 
    else
      this.copy(currentVolume = maybeNewVolume) 

  def empty() : Bottle = 
    this.copy(currentVolume = 0f)

  def isEmpty() : Boolean = 
    this.volume == 0f


object Bottle:
   def emptyMany(bottles : List[Bottle]) : List[Bottle] = 
     import language.experimental.fewerBraces
     bottles.map:
       x => 
         x.empty()

   def fillMany(bottles : List[Bottle], volume: Float) : List[Bottle] =
     for bottle <- bottles 
       yield bottle.fill(volume)

Zwar haben die beiden Code-Beispiele viele Ähnlichkeiten, jedoch sind die Unterschiede visuell sofort deutlich. Die Scala 3 „quiet Variante“ kommt ohne geschweifte Klammern aus. Bei der Übersetzung passieren viele Dinge, die wir nun im Detail betrachen werden.

Template-Bodies

Template-Bodies sind diejenigen Coderegionen, die beispielsweise den Rümpfen von Klassen-, Trait- und Objektdefinitionen folgen. In Scala 2 waren diese von geschweiften Klammer eingeschlossen, jetzt reicht ein Doppelpunkt nach dem Rumpf, wie im Beispiel in der Case-Class- und Objektdefinition zu sehen ist. Die folgende Einrückung macht die Zugehörigkeit zur Definition deutlich.

Einrückungen

Einrückungen bestimmen, wie die Zugehörigkeit von nachfolgenden Codezeilen zu einem Template-Body, aber auch einer Funktion, eines Zweiges oder einer Schleife bestimmt wird. Wir sehen dies in den Funktionsdefinitionen im Codebeispiel. Zudem finden wir in allen Kontrollstrukturen Code-Einrückungen, die Codezugehörigkeit definieren. Einrückungen in Scala 3 können sowohl durch Leerzeichen als auch Tabs beschrieben werden. Dabei können Leerzeichen und Tabs gemischt verwendet werden. Vier Tabs und zwei Leerzeichen zählen weniger als vier Tabs und drei Leerzeichen. Vier Tabs und zwei Leerzeichen sind äquivalent zu sechs Leerzeichen. Mancher kennt diese Möglichkeit der Kombination von Leerzeichen und Tabs zur Einrückung eventuell aus Haskell. Die Scala-Dokumentation selbst erwähnt, dass das Mischen der beiden nicht praktikabel ist und vermieden werden sollte.

Wem bei langen Regionen die Einrückung zu unübersichtlich ist, weil man das Ende der Region schwer identifizieren kann, der kann eine Ende-Markierung setzen. Dies geht mit dem Schlüsselwort end gefolgt von z.B. dem Identifier des Blocks. Um die Definition der Klasse Bottle aus dem Code-Beispiel abzuschließen, würden wir vor der Objektdefinition ohne Einrückung ein end Bottle einfügen. Die genauen Regeln können hier nachgelesen werden.

Kontrollstrukturen

Im obigen Beispiel sieht man im Scala 3-Code ein neues Schlüsselwort: then. Dieses Schlüsselwort ist nötig, um eine If-Anweisung klammerfrei eindeutig zu formulieren. Von diesen Schlüsselwörtern gibt es noch mehr:

  • die Bedingung einer while-Schleife wird von einem do abgeschlossen
  • der Rumpf einer for-Schleife kann sowohl von yield als auch do abgeschlossen werden. yield funktioniert wie bisher, beschreibt also eine monadische Anwendung, do hingegen definiert eine klassische for-Schleife ohne Rückgabewert (bzw. unit), wie im folgendes Beispiel skizziert:
// for-Schleife als monadische Anweisung 
for a <- List(1, 2, 3) 
   yield a * 2

// for-Schleife als imperative for-Schleife mit Rückgabewert unit
for a <- List(1, 2, 3) do
   val twiceA = a * 2
   println(twiceA)
   

Noch weniger Klammern

Im übersetzten Code sieht man im Companion-Object in der Methode emptyMany den Import des Packages fewerBraces. Dieses Feature ist ausgelagert, da es im Vorfeld noch kontroverser diskutiert wurde als die Syntaxumstellung ohnehin schon.

In Scala zwei gibt es die Möglichkeit, Statement-Blöcke und Definitionen von anonymen Funktionen in geschweiften Klammern vorzunehmen, wie im Code-Beispiel für Scala 2 in der emptyMany-Methode zu sehen. Mit dem Import von fewerBraces in der Scala 3-Variante dieser Methode können alle durch geschweifte Klammern definierten Blöcke von nun an mit einem Doppelpunkt eingeleitet und durch Einrückungen vom Rest abgegrenzt werden. Wird dieser Import nicht verwendet, gibt es keine Möglichkeit, die Definition der empty-many-Methode ohne geschweifte Klammern zu beschreiben.

Compiler, to the rescue!

Der Scala 3-Compiler kommt mit einer Handvoll Werkzeuge, um mit der neuen Syntax umzugehen. Der Compiler warnt den Benutzer, wenn Einrückungen inkonsistent sind, insbesondere, wenn die Klammersyntax verwendet wurde. Wer dieses Verhalten unterbinden und die klammerfreie Syntax abschalten möchte, kann dem Compiler die Flag -no-indent mitgeben.

Um Scala 2-Programme in neuer Syntax erstrahlen zu lassen, kann man den Compiler anweisen, diese umzuschreiben. Ausgeführt mit den Flags -rewrite und -indent ersetzt der Compiler wo möglich Klammern durch Einrückungen. Da dies nur in Verbindung mit der neuen Kontrollstrukturensyntax funktioniert, muss vorher ein Lauf mit -rewrite -new-syntax durchgeführt werden.

Fazit

Scala 3 ist nach langer Arbeit erschienen und das ist auch gut so. Allerdings haben wir die Reise in Scala 3 mit einem wenig motivierenden Thema begonnen, der optionalen neuen Syntax. Zwar ist die Syntax an sich nicht schlecht, doch Scala bietet wieder alle Möglichkeiten gleichzeitig an: die neue Syntax verwenden oder bei der alten bleiben? Leerzeichen verwenden oder Tabs oder beides? Optionale Features zuschalten?

Dies wird nicht für mehr Uniformität im Code führen sondern ohne strikten Formatierer viele Ausprägungen von Scala 3-Code hervorrufen, bis sich ein Quasistandard etabliert hat. Allerdings ist dies sehr unwahrscheinlich hinsichtlich der Kontroverse, zu der dieses Thema bereits im Vorfeld geführt hat.

In den folgenden Blogposts zu Scala 3 werden wir sinnvolle und spannende Features diskutieren, wie etwa Enums, Intersection- und Union-Types und die neuen implicits.