scalatutorial.de

Stufe 3Objekte und Typen · Lektion 17 von 34

Implizite Konvertierung, given und using

Aktualisiert am 24. September 20263 Min. LesezeitSchritt 5/6 dieser Stufe

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.

 Scala 3
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 überschreiben

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:

 Scala 3
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:

 Scala 3
import meinprojekt.instanzen.given          // alle givens
import meinprojekt.instanzen.{given Ordering[?]}   // nur bestimmte

So 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:

 Scala 3
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:

 Scala 3
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 2Scala 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): Bgiven Conversion[A, B]

Scala 3 versteht die alte Schreibweise weiterhin, damit Bibliotheken schrittweise umgestellt werden können.

Passt dazu