© Enkel/Shutterstock.com
Scala 3 Preview

Was passiert mit Implicits?


Mit dem Release von Scala 3 steht einiges an Veränderung vor der Tür. Unter anderem wurden Implicits, wie man sie aus Scala 2 kennt, gründlich überarbeitet. Die wichtigsten Änderungen und ihre Auswirkungen werden wir uns im folgenden Artikel ansehen.

Implicits sind ohne Zweifel eines der zentralen Features von Scala. Im Laufe der Zeit hat sich so einiges an Use Cases angesammelt, einige Beispiele dafür sind:

  • implizite Konvertierungen, um z. B. Werte zwischen Java und Scala zu konvertieren

  • implizite Variablen und Methoden in Verbindung mit impliziten Argumentlisten, um Parameter vom Compiler berechnen und übergeben zu lassen

  • Nutzung von Implicit Classes, um fremde Typen mit Funktionalität zu erweitern

  • verschiedenste Berechnungen auf dem Typlevel

  • und viele mehr …

Viele dieser Beispiele und Features waren nicht von Anfang an verfügbar, vielmehr wurde Scala nach der ersten Einführung von Implicits Schritt für Schritt ergänzt, immer wenn man neue Verwendungen entdeckte und sich diese etablierten.

Implicits haben im Laufe der Zeit aber auch einiges an Kritik angesammelt. Zum Beispiel gibt es mit impliziten Konvertierungen und impliziten Klassen mindestens zwei Möglichkeiten, fremde Typen mit Funktionalität zu erweitern. Und wer hat nicht schon viel Zeit damit verbracht, den richtigen Import zu finden, damit etwas kompiliert? Bei einer fehlgeschlagenen Suche nach verschachtelten Implicits steht man auch ohne jede Compilerhilfe da, denn der Fehler ist leider weder aussagekräftig noch hilfreich.

In Scala 3 wurde daher einiges an Arbeit investiert und das Konzept von Grund auf überarbeitet (Kasten: „Ein Wort zur Syntax“). Implicits sind immer noch ein fundamentales Feature der Sprache, aber die Use Cases, die sich in der Vergangenheit etabliert haben, werden nun sauber in Sprachfeatures abgebildet. Statt des implicit-Keywords für so ziemlich alles gibt es nun eigene fokussierte Features. Das macht es nicht nur unerfahrenen Programmieren um einiges leichter, auch erfahrene Scala-Entwickler werden die Änderungen zu schätzen wissen. Gleichzeitig kommt die Überarbeitung mit einer Vielzahl an Verbesserungen und hebt einige störende Limitierungen auf.

Ein Wort zur Syntax

Dieser Artikel verwendet die neue, optionale braceless Syntax von Scala 3. Natürlich ist es weiterhin möglich, die bekannte braceful Syntax mit mehr Klammern aus Scala 2 zu verwenden, beide werden vom Scala-3-Compiler unterstützt.

Weiterhin ist es wichtig zu wissen, dass Scala 3 (noch) sehr „nachsichtig“ ist, was die Syntaxänderungen zu Scala 2 angeht. So sind z. B. ein Großteil der alten Features und deren Syntax noch valide, werden jedoch in einer zukünftigen Version entfernt.

Ein Tipp an dieser Stelle: Will man einen strikteren Check beim Kompilieren, kann man dem Scala-Compiler den Flag -source:future übergeben. Dieser wurde für den gesamten Code in diesem Artikel verwendet.

Ein kurzer Überblick

Wir beginnen mit einem kurzen Überblick über die wichtigsten Themen, die wir im Rest des Artikels behandeln werden:

  • Verwendung von Given Instances statt implizitem val und def

    • neu: anonyme Deklarationen möglich

    • neu: explizites Importieren von Given Instances notwendig

  • Verwendung von Using Clauses statt impliziten Parametern

    • neu: wie bei given auch anonym möglich

    • neu: keine Beschränkung bezüglich Position und Anzahl

    • neu: explizite Aufrufe müssen markiert werden

  • Extension Methods statt impliziter Klassen

  • implizite Konvertierungen werden expliziter durch scala.util.Conversion

  • Context Functions als First-Class-Äquivalent zu Methoden mit Using Clauses

  • bessere Fehlermeldungen bei fehlgeschlagener Implicit-Suche

Given Instances

Als Erstes wollen wir einen Blick auf Given Instances werfen:

implicit val myEc: ExecutionContext = ??? // (1) implicit def encodeOption[A:Encoder]: Encoder[Option[A]] = ??? // (2)

Wenn man in Scala 2 einen Wert für die implizite Suche markieren will, geschieht das durch ein implicit val (1) bzw. implicit def (2) für den Fall, dass Typparameter oder Argumente benötigt werden.

In Scala 3 werden dafür Given Instances verwendet, die mit dem Keyword given deklariert werden. Allerdings ist given nicht das neue implicit, sondern es ersetzt einen spezifischen Use Case, das Deklarieren von kanonischen Werten. Diese können damit automatisch durch den Compiler gefunden und für Context-Parameter synthetisiert werden. Übersetzt in Scala 3 sieht das so aus:

given myEc: ExecutionContext = ??? // Alias Given (1) given ExecutionContext = ??? // Alias Given (2) given encodeOption[A:Encoder]: Encoder[Option[A]] = ??? // (3)

In (1) deklarieren wir eine Given Instance vom Typ ExecutionContext mit dem Namen myEc. Alternativ können wir den Namen auch automatisch vom Compiler generieren lassen und ihn aussparen, wie in (2) gezeigt. Eine Given Instance, die andere Parameter benötigt, kann auch Typparameter haben und die bekannten Context Bounds weiterhin benutzen. Diese führen zukünftig in Scala 3 aber nicht mehr zu einem zusätzlichen implicit-Parameter, sondern zu einem Context-Parameter in einer Using Clause. Im Alltag ist das Aussparen des Namens eine große Erleichterung, denn er spielt in den meisten Fällen sowieso keine Rolle, außer er ist schlecht.

Um das Importieren eines anonymen given zu ermöglichen, führt Scala 3 einen speziellen Selector ein, um diese Definitionen anhand eines Typselektors zu importieren. Standardmäßig werden keine given durch Wildcard-Imports mitselektiert. Hier die neue Syntax:

import de.codecentric.given // importiert alle given (1) import de.codecentric.{given Monad[?]} // importiert alle Monad-Instanzen (2)

Um alle Given Instances zu importieren, können wir den Import mit given beenden (1), somit sind alle Given Instances verfügbar. Möchten wir etwas feingranularer importieren, gibt es eine spezielle Selektorsyntax, die über den Typ funktioniert. Als Beispiel selektieren wir in (2) nur Given Instances, die Monaden definieren, beispielsweise Monad[List] oder Monad[Future].

Given Instances können statt mit einem = auch per with-Schlüsselwort deklariert werden. Dadurch kann man sich eine doppelte Typangabe sparen und direkt die benötigten Methoden deklarieren. In Listing 1 ist ein Beispiel zu sehen.

Listing 1 (Scala 3)

trait Pretty[A]: def pretty(a: A): String given...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang