Blog

Ableitung von Typklasseninstanzen mit Shapeless 3

Javier Martínez

Javier Martínez

Aktualisiert Oktober 15, 2025
7 Minuten

Scala 3 wurde mit neuen Mechanismen und Funktionen ausgestattet, die die Ableitung von Typklasseninstanzen ohne Verwendung von Makros oder Bibliotheken von Drittanbietern ermöglichen. Unter Automatisches Ableiten von Typklasseninstanzen in Scala 3 sehen wir, wie Sie diese neuen Werkzeuge wie Mirrors, Tupel oder Inlining verwenden können, um Instanzen der Typklasse Show abzuleiten.

Obwohl diese neuen Typen und Funktionen, die in die Sprache eingeführt wurden, sehr leistungsfähig sind, sind sie immer noch zu niedrig. Daher müssen Sie wissen, wie sie funktionieren und wie man sie miteinander verbindet, um die Ableitungslogik zu schreiben. In der Tat wird in der offiziellen Dokumentation darauf hingewiesen, dass dieses Framework für die Ableitung von Typklassen absichtlich klein und niedrigschwellig ist. Außerdem sagen die Autoren voraus, dass es für die Erstellung von Bibliotheken auf höherer Ebene hilfreicher sein könnte als für allgemeinen Code. Diese High-Level-Bibliotheken werden ergonomischere Lösungen für die Benutzer schaffen und alle Low-Level-Details verbergen. Shapeless 3 und Magnolia sind Beispiele für diese Art von Bibliotheken. In diesem Beitrag wird die bekannte Typklasse Show verwendet, um zu zeigen, wie man mit Shapeless 3 Instanzen ableiten kann.

Die Typklasse Show bietet eine Möglichkeit, einen Typ in eine Zeichenkette umzuwandeln. Sie können sie als eine Alternative zur Methode toString betrachten. Schauen wir uns an, wie sie aussieht.

trait Show[A]:
  def show(a: A): String

Der erste Schritt zur Ableitung von Instanzen für komplexe Typen besteht darin, Instanzen für die Basistypen zu schreiben, die wir unterstützen möchten.

object Show:
  given Show[Int] = _.toString
  given Show[Boolean] = _.toString
  given Show[String] = identity(_)

Wie Sie sehen können, haben wir die Instanzen im Begleitobjekt von Show definiert. Dies ist eine gute Vorgehensweise bei der Definition und Ableitung von Typklassen, da Sie die Instanzen jedes Mal im Gültigkeitsbereich haben, wenn Sie den Typ Show ohne einen zusätzlichen Import verwenden. Wenn Sie jedoch die Typklasse nicht besitzen oder andere Anforderungen haben, können Sie die Instanzen und die Ableitungslogik in einem separaten Objekt oder Trait definieren. Denken Sie aber daran, dass Sie in diesem Fall den Typ importieren oder erweitern müssen.

Um die Shapeless 3-Bibliothek zu verwenden, müssen wir sie wie üblich als Abhängigkeit zu unserem Projekt hinzufügen.

"org.typelevel" %% "shapeless3-deriving" % "version"

Die für diesen Beitrag verwendete Version ist 3.3.0.

Zusätzlich müssen wir den folgenden Import hinzufügen:

import shapeless3.deriving.*

Sobald wir die Basisinstanzen definiert und alles für die Verwendung von Shapeless 3 vorbereitet haben, besteht der nächste Schritt darin, Show-Instanzen für Produkttypen abzuleiten, die in Scala üblicherweise als Case-Klassen oder Tupel dargestellt werden.

def deriveShowProduct[A](using
    pInst: K0.ProductInstances[Show, A],
    labelling: Labelling[A]
): Show[A] =
  (a: A) =>
    labelling.elemLabels.zipWithIndex
      .map { (label, index) =>
        val value = pInst.project(a)(index)([t] => (st: Show[t], pt: t) => st.show(pt))
        s"$label = $value"
      }
      .mkString(s"${labelling.label}(", ", ", ")")

Dieser Ableitungsalgorithmus iteriert über alle Produktfeldnamen, die Sie von Labelling erhalten haben, zusammen mit ihren Indizes, um eine Zeichenkette für jedes Feld zu erhalten. Diese Zeichenkette besteht aus der Feldbezeichnung und der Stringprojektion des Feldwertes. Um die String-Projektion des Feldwerts zu erhalten, verwenden wir die Methode project, die in ProductInstances definiert ist:

inline def project[R](t: T)(p: Int)(f: [t] => (F[t], t) => R): R

Mit dieser Methode können wir ein Feld eines Produkttyps t nehmen, das durch seinen Index p ausgewählt wird, und es in einen anderen Typ R durch f umwandeln. Wie Sie vielleicht bemerken, ist kein gewöhnliches Lambda; das liegt daran, dass es sich um einen polymorphen Funktionstyp handelt, einen neuen Typ, der in Scala 3 eingeführt wurde und der eine Funktion beschreibt, die Typparameter haben kann. Diese Funktion ermöglicht es, die Instanz der Typklasse F[t], in diesem Fall Show[t], und den Wert des von uns ausgewählten Feldes zu verwenden, um es zu transformieren.

Schließlich zeigen wir mit der Methode mkString alle Feldzeichenketten in einem String an.

ProductInstances und Labelling sind zwei High-Level-Komponenten, die Shapeless zur Verfügung stellt, um uns bei der Ableitung von Typklasseninstanzen zu helfen. Erstere führt eine Reihe von Primitiven ein, wie z.B. project, construct oder foldLeft, und macht das Schreiben der Ableitungslogik vieler Typklassen sehr einfach. Wie der Name schon sagt, ist nur für Produkttypen verfügbar, während wir Instanzen von sowohl für Produkt- als auch für Summentypen erhalten können. Die letztgenannte Komponente, Labelling, liefert nützliche Informationen: den Namen des Typs und die Namen der Typmitglieder.

Die Ableitung von Instanzen für Summentypen, auch Koprodukte genannt und in Scala 3 als versiegelte Traits oder Enums dargestellt, ist sogar noch einfacher als für Produkttypen.

def deriveShowSum[A](using cInst: K0.CoproductInstances[Show, A]): Show[A] =
  (a: A) => cInst.fold(a)([a] => (st: Show[a], a: a) => st.show(a))

Wir müssen nur die Methode fold verwenden, die in CoproductInstances definiert ist. Die Methode fold ermöglicht die Verwendung der entsprechenden Instanz der Typklasse jedes Mitglieds des Koprodukts, um dessen Wert anzuzeigen. Wie Sie vielleicht schon vermutet haben, ist CoproductInstances analog zu ProductInstances, allerdings für Summentypen.

An diesem Punkt haben wir den notwendigen Code geschrieben, um Show Instanzen für die Typen Product und Sum zu erstellen. Allerdings haben wir nur zwei Methoden definiert, und der Compiler kann immer noch nicht automatisch die Instanzen ableiten. Um dieses Problem zu lösen, müssen wir eine kleine, aber wichtige Methode schreiben.

inline given derived[A](using gen: K0.Generic[A]): Show[A] =
    gen.derive(deriveShowProduct, deriveShowSum)

Der Name dieser Methode wurde nicht zufällig gewählt, sondern um den Vertrag der derives Klausel des Scala-Compilers zu erfüllen. Das bedeutet, dass diese Methode verwendet wird, wenn der Compiler derives Show in einem Ihrer Typen findet. Darüber hinaus gibt es noch eine weitere Situation, in der diese Methode automatisch aufgerufen wird: wenn der Compiler eine Instanz von Show[A] aufrufen muss, da diese gegeben ist.

Aber wie funktioniert diese Methode? Nun, sie ist ganz einfach. Sie entscheidet je nach Typ, welche Ableitungslogik verwendet werden soll. Wenn A ein Produkttyp ist, wird die Methode deriveShowProduct verwendet, um die Instanz abzuleiten. Wenn A ein Summentyp ist, wird stattdessen die Methode deriveShowSum verwendet.

Das folgende Diagramm zeigt den High-Level-Fluss, dem der Scala-Compiler folgt, wenn er eine Instanz von Show für einen bestimmten Typ aufrufen muss.

In der Methode derived verwenden wir eine andere Typklasse, Generic, die von Shapeless implementiert wird, wodurch die Implementierung der Derivatmethode sehr einfach ist. Natürlich hat Generic mehr Verwendungsmöglichkeiten und verwendet hinter den Kulissen fortgeschrittene Mechanismen auf Typebene. Wenn Sie Shapeless 2 verwendet haben, sollte Ihnen dieser Typ vertraut sein.

Wir fügen alles zusammen:

import shapeless3.deriving.*
trait Show[A]:
  def show(a: A): String
object Show:
  given Show[Int] = _.toString
  given Show[Boolean] = _.toString
  given Show[String] = identity(_)
  def deriveShowProduct[A](using
    pInst: K0.ProductInstances[Show, A],
    labelling: Labelling[A]
  ): Show[A] =
    (a: A) =>
      labelling.elemLabels.zipWithIndex
        .map { (label, index) =>
          val value = pInst.project(a)(index)([t] => (st: Show[t], pt: t) => st.show(pt))
          s"$label = $value"
        }
        .mkString(s"${labelling.label}(", ", ", ")")
  def deriveShowSum[A](using
      inst: K0.CoproductInstances[Show, A]
  ): Show[A] =
    (a: A) => inst.fold(a)([a] => (st: Show[a], a: a) => st.show(a))
  inline given derived[A](using gen: K0.Generic[A]): Show[A] =
    gen.derive(deriveShowProduct, deriveShowSum)

Wir können nicht aufhören, ohne unseren Ableitungscode auszuprobieren.

final case class Foo(x: Int, y: String, z: Boolean)
enum ColorEnum:
  case Red, Green, Blue
println(summon[Show[Foo]].show(Foo(1, "s", true)))
println(summon[Show[ColorEnum]].show(ColorEnum.Blue))

Das Ergebnis ist:

"Foo(x = 1, y = s, z = true)"
"Blue()"

Wie wir gesehen haben, bietet Shapeless 3 High-Level-Abstraktionen zur einfachen und eleganten Ableitung von Typklasseninstanzen für Scala 3. Unter der Haube nutzt es das neue Framework für die Ableitung von Typklassen und andere Kompilierzeit- und Metaprogrammierfunktionen von Scala 3.

Das Github Repo für diesen Beitrag enthält den gesamten hier gezeigten Code. Außerdem enthält es den Code für die Ableitung von Instanzen von Show nur mit Scala 3, so dass Sie beide Ansätze vergleichen und ausprobieren können. Viel Spaß!

Verfasst von

Javier Martínez

Contact

Let’s discuss how we can support your journey.