Stufe 3Objekte und Typen · Lektion 17 von 34
Implizite Konvertierung, given und using
Kaum ein Merkmal von Scala war so mächtig und zugleich so gefürchtet wie das Schlüsselwort implicit. Scala 3 hat es in klar getrennte Werkzeuge aufgeteilt: given und using für Kontextparameter, extension für neue Methoden und Conversion für echte Typumwandlungen.
Das Problem: Kontext, der überall gebraucht wird
Manche Werte werden an sehr vielen Stellen benötigt, ändern sich aber selten: eine Datenbankverbindung, eine Spracheinstellung, ein Thread-Pool. Sie überall ausdrücklich zu übergeben, bläht jede Signatur auf. Kontextparameter lösen das: Der Compiler reicht den Wert automatisch weiter.
case class Sprache(code: String)
def begruesse(name: String)(using s: Sprache): String =
s.code match
case "de" => s"Hallo, $name"
case "fr" => s"Bonjour, $name"
case _ => s"Hello, $name"
given Sprache = Sprache("de")
begruesse("Ada") // "Hallo, Ada" – Sprache kommt aus dem given
begruesse("Ada")(using Sprache("fr")) // ausdrücklich überschreibenusingmarkiert eine Parameterliste als kontextabhängig.givenstellt einen Wert bereit, den der Compiler für diesen Typ einsetzt.- Gesucht wird im aktuellen Gültigkeitsbereich, in importierten givens und in den Begleitobjekten der beteiligten Typen.
Findet der Compiler keinen passenden Wert, bricht er mit einer klaren Meldung ab: No given instance of type Sprache was found.
Typklassen
Das wichtigste Einsatzgebiet von givens sind Typklassen. Eine Typklasse beschreibt eine Fähigkeit, die Typen nachträglich bekommen können – ohne dass die Typen davon wissen müssen:
trait Anzeigbar[A]:
extension (a: A) def anzeigen: String
given Anzeigbar[Int] with
extension (n: Int) def anzeigen = s"Zahl $n"
given Anzeigbar[Boolean] with
extension (b: Boolean) def anzeigen = if b then "ja" else "nein"
given [A](using Anzeigbar[A]): Anzeigbar[List[A]] with
extension (xs: List[A]) def anzeigen = xs.map(_.anzeigen).mkString("[", ", ", "]")
def drucke[A: Anzeigbar](a: A): Unit = println(a.anzeigen)
drucke(42) // Zahl 42
drucke(List(true, false)) // [ja, nein]Das dritte given ist besonders elegant: Es baut aus einer Instanz für A eine für List[A]. Der Compiler setzt solche Instanzen selbstständig zusammen. Die Schreibweise A: Anzeigbar ist ein Context Bound und bedeutet nichts anderes als (using Anzeigbar[A]).
Die Standardbibliothek nutzt Typklassen zum Beispiel für Ordering (Sortieren), Numeric (Rechnen) und CanEqual (typsichere Gleichheit). Deshalb funktioniert List(3, 1, 2).sorted – für Int gibt es ein given Ordering[Int].
givens importieren
Ein normales import paket.* importiert givens nicht. Dafür gibt es eine eigene Form:
import meinprojekt.instanzen.given // alle givens
import meinprojekt.instanzen.{given Ordering[?]} // nur bestimmteSo ist beim Lesen einer Datei sofort erkennbar, woher Kontextwerte stammen – ein häufiger Kritikpunkt an Scala 2.
Extension Methods
Wie Sie bestehenden Typen neue Methoden geben, haben Sie in der Lektion über Funktionen schon gesehen:
extension (i: Int)
def mal(f: => Unit): Unit = for _ <- 1 to i do f
3.mal { println("Scala") }In Scala 2 brauchte man dafür eine implicit class. Die Absicht war dieselbe, aber im Code nicht auf den ersten Blick zu erkennen.
Implizite Konvertierungen
Eine implizite Konvertierung wandelt einen Wert automatisch in einen anderen Typ um, wenn der erwartete Typ nicht passt. In Scala 3 ist sie eine Instanz von Conversion:
import scala.language.implicitConversions
case class Meter(wert: Double)
given Conversion[Int, Meter] = n => Meter(n.toDouble)
def laufe(strecke: Meter): String = s"${strecke.wert} m gelaufen"
laufe(400) // "400.0 m gelaufen"Solche Konvertierungen sind bewusst unbequem gemacht: Ohne den Sprach-Import warnt der Compiler. Denn automatische Umwandlungen machen Code schwer nachvollziehbar – plötzlich passiert etwas, das im Quelltext nicht steht. In den allermeisten Fällen sind eine explizite Fabrikmethode oder eine Extension Method die bessere Wahl.
So sah es in Scala 2 aus
Beim Lesen von älterem Code hilft diese Übersetzungstabelle:
| Scala 2 | Scala 3 |
|---|---|
implicit val x: T = … | given T = … |
def f(implicit t: T) | def f(using t: T) |
implicitly[T] | summon[T] |
implicit class RichInt(i: Int) | extension (i: Int) |
implicit def a2b(a: A): B | given Conversion[A, B] |
Scala 3 versteht die alte Schreibweise weiterhin, damit Bibliotheken schrittweise umgestellt werden können.