Blog

Warum zuerst kontextuelle Abstraktionen in Scala 3 lernen?

Esteban Marin

Aktualisiert Oktober 15, 2025
11 Minuten

TL;DR: Scala 2 ist laut der Scala-Entwicklerumfrage immer noch die am häufigsten verwendete Version von Scala. Das bedeutet, dass die Kontextabstraktionen Scala-Entwicklern, insbesondere Neulingen, immer noch Kopfzerbrechen bereiten. In diesem Blog werden kontextbezogene Abstraktionen vorgestellt, zunächst aus der Sicht von Scala 3, das von der Überarbeitung der API profitiert. Dann werden wir die Intuition der kontextuellen Abstraktionen mit einigen Codebeispielen verdeutlichen und mit der Beziehung zu Scala 2 abschließen. So können Sie die Designprinzipien der kontextuellen Abstraktionen von Scala 3 bei der Arbeit an Scala 2-Codebases nutzen, um Fallstricke zu vermeiden und Code Smells zu erkennen, aber auch um sie leichter zu verdauen.

Was sind kontextuelle Abstraktionen?

Das Implizite von Scala ist eine der wichtigsten Funktionen der Sprache. Es ist das grundlegende Werkzeug zur Abstraktion über den Kontext und hat viele Anwendungsfälle wie die Implementierung von Typklassen, Injektion von Abhängigkeiten, Ausdruck von Fähigkeiten, Berechnung neuer Typen und mehr. Andere beliebte Sprachen wie Haskell, Rust und Swift implementieren die gleiche Idee.

Der Kern der Intuition für kontextuelle Abstraktionen ist die Idee der Begriffsinferenz: Wenn ein Typ gegeben ist, kann der Compiler ein Standardobjekt erstellen, das diesen Typ hat.

Kritiken

  • Sie sind zu mächtig, und man kann sie leicht missbrauchen oder überstrapazieren.
  • Der Missbrauch von implicit Importen führt oft zu schwer nachvollziehbaren Fehlern.
  • Die Syntax der kontextuellen Abstraktionen ist minimal; sie besteht aus einem einzigen implicit Modifikator, der an viele Sprachkonstrukte angehängt werden kann. Das bedeutet, dass die Verwendung für Neulinge verwirrend ist.
  • Kontextfunktionen stellen eine Herausforderung für Tools wie Scaladocs dar, die auf statischen Webseiten beruhen.
  • Fehlgeschlagene Suchen mit Kontextfunktionen liefern wenig hilfreiche Meldungen, insbesondere wenn eine tief rekursive implizite Suche fehlgeschlagen ist.

Keines davon stellt jedoch ein fatales Problem dar, denn die heutigen Scala-Entwickler haben sich daran gewöhnt. Dieser Status Quo stellt eine große Hürde für Normalsterbliche dar.

Was ist neu in Scala 3

Scala 3 führt vier grundlegende Änderungen für kontextuelle Abstraktionen ein:

  1. Gegebene Instanzen : Neue Methode zur Definition "kanonischer" Werte eines Typs.
  2. Klauseln verwenden : Dies bedeutet implicit Argumente, die mit dem Schlüsselwort using definiert wurden.
  3. Gegebene Importe : Eine spezielle Form des Import-Wildcard-Selektors wird verwendet, um bestimmte Instanzen zu importieren.
  4. Implizite Umrechnungen : Implizite Konvertierungen werden durch bestimmte Instanzen der Klasse scala.Conversion definiert.

Diese Designgrundlagen vermeiden Interaktionen zwischen anderen Sprachfunktionen und sorgen für mehr Orthogonalität, da sie eine bessere Begriffsinferenz von anderen Sprach-APIs trennen. Um es auf den Punkt zu bringen, besteht die Überarbeitung der API aus (nicht umfangreichen):

  • Eine einzige Möglichkeit, givens mit einem extension Schlüsselwort für die Methodenerweiterung zu definieren, und es gibt eine einzige Möglichkeit, implizite Parameter und Argumente einzuführen
  trait Ord[T]:
  def compare(x: T, y: T): Int
  extension (x: T) 
    def < (y: T) = compare(x, y) < 0
    def > (y: T) = compare(x, y) > 0

    given intOrd: Ord[Int] with
    def compare(x: Int, y: Int) =
        if x < y then -1 else if x > y then +1 else 0

    given listOrd[T](using ord: Ord[T]): Ord[List[T]] with

    def compare(xs: List[T], ys: List[T]): Int = (xs, ys) match
        case (Nil, Nil) => 0
        case (Nil, _) => -1
        case (_, Nil) => +1
        case (x :: xs1, y :: ys1) =>
        val fst = ord.compare(x, y)
        val extensionUsage = x > y 
        if fst != 0 then fst else compare(xs1, ys1)
  • Es gibt eine einzige Möglichkeit, implizite Werte zu importieren
     object A:
        class TC
        given tc: TC = ???
        def f(using TC) = ???

    object B:
        import A.*
        import A.given
  • Und für die verwendeten und missbrauchten impliziten Konvertierungen, prägnante Absicht
import scala.Conversion
abstract class Conversion[-T, +U] extends (T => U):
  def apply (x: T): U
given Conversion[String, Token] with
  def apply(str: String): Token = new KeyWord(str)
given Conversion[String, Token] = new KeyWord(_)

Code Beispiele

Lassen Sie uns nun tiefer in die kontextuellen Abstraktionen eintauchen, indem wir mit etwas Code und Typeclasses herumspielen. Wenn Sie alle Details erfahren möchten, sollten Sie das fp-fundamentals Repo klonen, um besser folgen zu können.

Schritt 0: Gerüstbau

Wie Sie sehen können, ist der folgende Code zwingend erforderlich und gibt das Konto balance auf der Konsole aus. Wir werden funktionale Programmierung und Typklassen verwenden, um diesen Codeschnipsel zu verbessern.

@main def hello: Unit =
  def getBalanceBank1: Int = 100
  def getBalanceBank2: Int = 80
  def balance: Int =
    val b1: Int = getBalanceBank1
    val b2: Int = getBalanceBank2
    b1 + b2
  // EDGE OF THE WORLD
  println(balance)

Schritt 1: Datentyp Maybe erstellen

Lassen Sie uns einen Maybe Datentyp erstellen, um die Abfrage über die Methode def getBalance... besser auszudrücken. Lassen Sie uns auch das Programm aktualisieren.

sealed trait Maybe[+A]
  case class Yes[A](a: A) extends Maybe[A]
  case object No extends Maybe[Nothing]
@main def hello: Unit =
- def getBalanceBank1: Int = 100
- def getBalanceBank2: Int = 80
+ def getBalanceBank1: Maybe[Int] = Yes(100)
+ def getBalanceBank2: Maybe[Int] = Yes(80)
  def balance: Int =
-   val b1: Int = getBalanceBank1
-   val b2: Int = getBalanceBank2
    b1 + b2
+   val b1: Maybe[Int] = getBalanceBank1
+   val b2: Maybe[Int] = getBalanceBank2
    b1 + b2 // it will fail
  // EDGE OF THE WORLD
  println(balance)

Schritt 2: Erstellen Sie die Typklasse Combinable

Wir erstellen eine kombinierbare Typklasse, um die zuvor definierte Maybe Typklasse zu kombinieren. In dem Instanzen-Objekt werden wir die given Instanzen erstellen.

  trait Combinable[A]:
   def combine(x: A, y: A): A

  given Combinable[Int] with
    override def combine(x: Int, y: Int): Int = x + y

  given [A: Combinable]: Combinable[Maybe[A]] with
    override def combine(x: Maybe[A], y: Maybe[A]): Maybe[A] = (x, y) match {
       ...
    }

Und jetzt wird unser Code funktionieren, indem wir das Programm auf summon[Combinable[Maybe[Int]]] aktualisieren.

+ package training
+ import training.DataTypes.*
+ import training.Typeclases.*
+ import training.Instances.given

@main def hello: Unit =
  def getBalanceBank1: Maybe[Int] = Yes(100)
  def getBalanceBank2: Maybe[Int] = Yes(80)
-  def balance: Int =
-    val b1: Maybe[Int] = getBalanceBank1
-    val b2: Maybe[Int] = getBalanceBank2
-    b1 + b2 // it will fail
+  val b1: Maybe[Int] = getBalanceBank1
+  val b2: Maybe[Int] = getBalanceBank2
+  val value: Maybe[Int] = summon[Combinable[Maybe[Int]]].combine(b1, b2)

  // EDGE OF THE WORLD
  println(balance)
//   println(balance)

Schritt 3: Syntax für Combinable

Lassen Sie uns sehen, wie wir eine Erweiterungsmethode erstellen können. Früher, in Scala 2, haben Sie etwas deklariert wie:

implicit class CircleDecorator(c: Circle) extends AnyVal {
  def circumference: Double = c.radius * math.Pi * 2
}

Jetzt verwenden wir das Schlüsselwort extension. Beachten Sie, dass der Compiler etwas Ähnliches wie im letzten Beispiel kompilieren wird. Sie als Benutzer werden jedoch davon abstrahiert.

extension [A: Combinable](aMaybe: Maybe[A])
    def +(bMaybe: Maybe[A]): Maybe[A] = (aMaybe, bMaybe) match

Dann aktualisieren wir unsere Program.scala

-  val value: Maybe[Int] = summon[Combinable[Maybe[Int]]].combine(b1, b2)
  val value: Maybe[Int] = b1 + b2

Jetzt kümmert sich die Syntax für uns um die Beschwörung, indem sie die Erweiterungsmethode verwendet. Großartig! Lassen Sie uns weitermachen. Es gibt noch eine Menge zu tun.

Schritt 4: Erstellen Sie einen Typklassen-Transformer (mit Syntax)

Dies ist die neue Typklasse, die wir einführen werden und die uns helfen wird, jedem Wert, der in einer bestimmten Typklasse enthalten ist, Transform-Eigenschaften hinzuzufügen.

trait Transformable[F[_]]:
    def map[A, B](fa: F[A], f: A => B): F[B]
// lets add an extension method
  extension [A](self: Maybe[A])
    def map[B](f: A => B): Maybe[B] = summon[Transformable[Maybe]].map(self, f)

Und jetzt können wir map unsere Werte in unserem Program.scala

- val b1: Maybe[Int] = getBalanceBank1
+ val b1: Maybe[Int] = getBalanceBank1.map(_.balance)

Schritt 5: Erstellen Sie eine bessere Version der vorherigen Typklasse.

Wir werden unserer Domain Statement eine neue case class hinzufügen, da String nicht mehr ausreicht.

@main def hello: Unit =
   case class Account(id: String, balance: Int)
+  case class Statement(isRich: Boolean, accounts: Int)
...

Wir können nun eine Statement statt einer Bilanz in unserer Program.scala erstellen.

- val balance: Maybe[Int] = b1 + b2
+ def statement =
+   summon[Transformable[Maybe]].map2(b1, b2, (x: Int, y: Int) => Statement(x + y > 1000, 2))
  // EDGE OF THE WORLD
- println(balance)
+ println(statement)

Schritt 6: Erstellen Sie die Typklasse Liftable

Auch hier erstellen wir eine neue Typklasse. Nehmen wir an, wir haben einen weiteren Datenstrom mit dem Namen moneyInPocket und möchten dieses Geld zu unserer Statement Berechnung hinzufügen.

Für Scala 3:

 trait Liftable[F[_]]:
    def pure[A](a: A): F[A]
// code abreviated
    ...
extension [A](self: A)
    def pure[F[_]](using sv: Liftable[F]): F[A] = sv.pure(self)

Für Scala 2 hätten wir dann:

 implicit class LiftableSyntax[A](self: A) {
    def pure[F[_]](implicit ev: Liftable[F]): F[A] = ev.pure(self)
  }

Der Unterschied in der Art und Weise, wie man eine Erweiterungsmethode ausdrückt, ist in Scala 3 deutlicher als in Scala 2. Wenn man den Code von Scala 2 liest, muss man intuitiv davon ausgehen, dass der Programmierer es als Erweiterungsmethode gemeint hat, als er implicit class schrieb. Aber man könnte class leicht mit var oder def verwechseln, vor allem als Neueinsteiger. Scala 3 korrigiert dies durch die Benennung von extension, so dass Sie keinen Fehler machen, wenn Sie versuchen, sie zu verwenden.

Wenn wir unser Program.scala aktualisieren, haben wir nun die Möglichkeit, jeden beliebigen Wert in unser Programm zu übernehmen:

  def getBalanceBank1: Maybe[Account] = Yes(Account("a1", 100))
  def getBalanceBank2: Maybe[Int] = Yes(80)
+ val moneyInPocket: Int = 20

  val b1: Maybe[Int] = getBalanceBank1.map(_.balance)
  val b2: Maybe[Int] = getBalanceBank2
  val balance: Maybe[Int] = b1 + b2
  ...
+ val p: Maybe[Int] = moneyInPocket.pure
+ val balance: Maybe[Int] = b1 + b2 + p

Weiter geht's.

Schritt 7: Erstellen einer Typklasse Flattener

Falls Sie es noch nicht bemerkt haben, sind wir fast fertig mit allen Monaden-Tutorials, die es gibt. Lassen Sie uns diese mystische Flatterner Eigenschaft erstellen:

  trait Flattener[F[_]]:
    def flatMap[A, B](fa: F[A], f: A => F[B]): F[B]

Warum die Typsignatur der Methode def flatMap so aussieht, werden wir so hinnehmen, wie sie ist. Aber wir haben jetzt die Möglichkeit, effektiv auf den enthaltenen Wert zuzugreifen, unabhängig davon, was das Wort effect bedeutet. Es ist erwähnenswert, dass es auch eine Intuition der Sequenzialität gibt, die durch diese Eigenschaft gegeben ist.

Für unser Arbeitsbeispiel können wir Program.scala auf diese Weise aktualisieren:

  case class Account(id: String, balance: Int)
  case class Statement(isRich: Boolean, accounts: Int)

- def getBalanceBank1: Maybe[Account] = Yes(Account("a1", 100))
+ def getBank1Credentials: Maybe[String] = Yes("MyUser_MyPassword")
+ def getBalanceBank1(credentials: String): Maybe[Account] = Yes(Account("a1", 100))
  def getBalanceBank2: Maybe[Int] = Yes(80)
  val moneyInPocket: Int = 20

- val b1: Maybe[Int] = getBalanceBank1.map(_.balance)
+ val b1: Maybe[Int] = getBank1Credentials.flatMap(cred => getBalanceBank1(cred).map(_.balance))
  val b2: Maybe[Int] = getBalanceBank2
  val p: Maybe[Int] = moneyInPocket.pure
  val balance: Maybe[Int] = b1 + b2 + p

Schritt 8: Monadische Struktur verwenden

Ja, das Clickbait des M-Worts. Das bedeutet, dass wir jetzt for-comprehensions verwenden und unsere Datenstrukturen sequentiell zusammenstellen können. In unserem Program.scala würde das so aussehen:


- val b1: Maybe[Int] = getBank1Credentials.flatMap(cred => getBalanceBank1(cred).map(_.balance))
- val b2: Maybe[Int] = getBalanceBank2
- val p: Maybe[Int] = moneyInPocket.pure
- val balance: Maybe[Int] = b1 + b2 + p

+  val balance =
+    for
+      c <- getBank1Credentials
+      b1 <- getBalanceBank1(c).map(_.balance)
+      b2 <- getBalanceBank2
+      p <- moneyInPocket.pure
+    yield b1 + b2 + p

Die Verbesserungen können fortgesetzt werden, da wir weiterhin Typklassen einführen können, um Traits in unseren Programmen zu ergänzen und zu beschreiben. Wir belassen es vorerst dabei, aber wenn Sie mehr Details benötigen, lesen Sie bitte im Repository nach. Lesen Sie mehr über Typklassen, noch mehr überTypklassenBeratung und machen Sie einen Rundgang durch die funktionalen Typklassen.

Simulieren von Scala 3 Contextual Abstractions-Konzepten mit impliziten Scala 2-Konzepten

Gegebene Instanzen:

In Scala 3,

given intOrd: Ord[Int] with { ... }

bildet in Scala 2 zu

implicit object intOrd extends Ord[Int] { ... }

Erweiterungsmethoden

In Scala 3,

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2

bildet in Scala 2 zu

implicit class CircleDecorator(c: Circle) extends AnyVal {
  def circumference: Double = c.radius * math.Pi * 2
}

Parametrisierte Gegebenheiten

In Scala 3 haben wir es wie folgt beschrieben,

given listOrd[T](using ord: Ord[T]): Ord[List[T]] with { ... }

In Scala 2 hingegen müssen wir etwas Gymnastik betreiben:

class listOrd[T](implicit ord: Ord[T]) extends Ord[List[T]] { ... }
final implicit def listOrd[T](implicit ord: Ord[T]): listOrd[T] =
 new listOrd[T]

Dies ist keine erschöpfende Auflistung. Sehen Sie sich daher unbedingt die offizielle Dokumentation an.

Fazit

Die kontextuellen Abstraktionen sind eine der leistungsfähigsten Funktionen von Scala. Nur wenige Sprachen haben sie, und ihre Überarbeitung macht sie attraktiver und leichter zu erlernen. Wir haben die Definition der kontextuellen Abstraktionen, ihre Kritik an der Scala 2-Implementierung, die Einführung von vier grundlegenden Änderungen, die die Sprachorthogonalität von Scala 3 verbessern, und Codebeispiele für Typklassen besprochen. Ich denke, man kann mit Sicherheit sagen, dass die kontextuelle Abstraktion in Scala 3 im Vergleich zu Scala 2 leichter zu verdauen ist. Außerdem bietet es tiefere Einblicke bei der Fehlersuche in Ihrem Scala 2 Implicits Compiler Problem.

Verfasst von

Esteban Marin

Scala Developer. Functional programming enthusiast.

Contact

Let’s discuss how we can support your journey.