Blog

Scala und Kotlin unter einem Dach

Alejandro Serrano Mena

Aktualisiert Oktober 15, 2025
8 Minuten

In einem unserer jüngsten Open-Source-Projekte haben wir Kotlin- und Scala-Code in einem einzigen Gradle-Projekt kombiniert. Da beide Sprachen mit der Java Virtual Machine arbeiten, waren wir zuversichtlich, dass wir das schaffen würden. In diesem Beitrag beschreiben wir einige der Probleme, auf die wir auf unserem Weg gestoßen sind, und was wir gelernt haben, um sie zu lösen.

Wie wir dorthin kamen

Karat ist eine neue Bibliothek, die wir entwickeln, um die Macht der zeitlichen Logik für das Testen zu nutzen. Kurz gesagt bedeutet das, dass Sie Sequenzen von Aktionen ausdrücken können, wie z.B. "zu einem bestimmten Zeitpunkt, nachdem diese HTTP-Anfrage eingegangen ist, sollte eine weitere Nachricht dieser Form in dieser Kafka-Warteschlange vorhanden sein." Dies erweitert das eigenschaftsbasierte Testen, indem es die Aktionen auch für die Randomisierung verfügbar macht. Sowohl Kotlin als auch Scala verfügen über ausgereifte und bekannte Bibliotheken für eigenschaftsbasierte Tests, nämlich Kotest und ScalaCheck. Wir haben uns entschlossen, diese so eng wie möglich zu integrieren, um den Benutzern die Möglichkeit zu geben, temporale Logik in einem Setup zu verwenden, mit dem sie bereits vertraut sind.

Das Herzstück dieser Art des Testens ist ein Algorithmus, der entscheidet, ob eine bestimmte temporale Formel an einem bestimmten Punkt in der Sequenz wahr ist und wie wir im nächsten Schritt fortfahren sollten (der interessierte Leser kann die Details in diesem Papier über Quickstrom nachlesen). Es war klar, dass eine Duplizierung dieses Codes und des gesamten Frameworks zur Beschreibung der zeitlichen Formeln nicht wünschenswert war. Außerdem wollten wir nicht mehrere verschiedene Projekte für jede der Integrationen mit Bibliotheken verwalten. Wir entschieden uns dafür, den gemeinsamen Code und die Integrationen in einem einzigen Repository zu hosten.

Projekt einrichten

Scala-Projekte verwenden häufig SBT als Build-Tool, während Kotlin-Entwickler fast ausschließlich Gradle verwenden. Glücklicherweise bündelt Gradle ein Scala-Plugin. Wir hatten keine Probleme damit, den gemeinsamen Kern von Kotlin in den Scala-Code zu übernehmen.

Multiplattform

Sowohl Kotlin als auch Scala unterstützen mehrere Ziele für die Kompilierung. Kotlin verwendet das übergreifende Kotlin Multiplatform, Scala verwendet Geschwisterprojekte wie Scala.js und Scala Native. Ursprünglich dachten wir, dass die Zusammenarbeit all dieser Plattformen zu schwierig wäre, um einen kleinen Gewinn zu erzielen, also haben wir uns zunächst die JVM als Ziel gesetzt.

Irgendwann beschlossen wir, die gemeinsame Bibliothek in ein Kotlin Multiplattform-Projekt umzuwandeln, um die gleichen Plattformen wie Kotest und Ktor anzusprechen. Zu unserer (angenehmen) Überraschung war Gradle in der Lage, die JVM-Version korrekt für die Scala-Plattform bereitzustellen, was keine weiteren Anpassungen erforderte.

Scala und Binärkompatibilität

Leider hat Scala 2 eine komplizierte Binärkompatibilität (die mit Scala 3 gelöst wurde). In der Praxis bedeutet das, dass jede Bibliothek mehrmals kompiliert werden muss, einmal für jede unterstützte Compiler-Version. Das ist etwas, das Sie bei der Verwendung von SBT nicht bemerken,

libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.15" % "test"

wird aber in den Artefakten, die in Maven Central veröffentlicht werden, als Suffix angezeigt. Die einzelne ScalaCheck-Bibliothek wird zu scalacheck_2.13 und scalacheck_3.

Gradle versteht diese Konvention nicht. Wir begannen damit, uns auf eine einzige Version der Bibliothek zu verlassen, was gut genug war, um Fortschritte zu machen. Als wir uns jedoch mit der Veröffentlichung der Bibliothek befassten, wurde uns klar, dass wir für jede Compiler-Version von der richtigen Bibliothek abhängig sein mussten. Glücklicherweise gibt es ein Scala Multi-Version Plug-in für Gradle, das dieses Problem löst. Wenn Sie Ihre Gradle-Abhängigkeiten deklarieren, verwenden Sie %% für die Compiler-Version; so sieht es bei der Verwendung von Versionskatalogen aus.

scalacheck-core = { module = "org.scalacheck:scalacheck_%%", version.ref = "scalacheck" }

Dieses Plug-in kümmert sich auch um die Paketierung und Veröffentlichung verschiedener Artefakte, je nachdem, welche Scala-Versionen Sie als unterstützt deklarieren.

JUnit-Plattform vs. MUnit vs. JUnit

Kotest verwendet den JUnit 5 Runner, was bedeutet, dass Sie useJUnitPlatform einbinden müssen, um diese Tests für Gradle verfügbar zu machen.

tasks.withType<Test>().configureEach {
  useJUnitPlatform()
}

ScalaCheck verfügt nicht über eine direkte Integration mit JUnit, aber wir haben herausgefunden, dass wir ScalaCheck über MUnit ausführen können, das es unterstützt. Dies erfordert einige Änderungen an der Art und Weise, wie Sie die Eigenschaften deklarieren (Sie können die Properties), aber es war insgesamt eine kleine Änderung. Nachdem ich alles eingerichtet hatte, wurden die Tests leider nicht entdeckt!

Es stellt sich heraus, dass MUnit stattdessen den JUnit 4-Runner verwendet. Dies erfordert eine kleine Änderung im Scala-Projekt.

  tasks.withType<Test>().configureEach {
-   useJUnitPlatform()
+   useJUnit()
  }

Generika

Java unterstützt Generika, Scala unterstützt Generika, Kotlin unterstützt Generika, was soll also die ganze Aufregung? Das Problem ist, dass Java eine andere Herangehensweise an die Varianz hat als Scala und Kotlin, und das zieht sich durch das gesamte Ökosystem.

Bevor wir weitermachen, unterstützt Scala eine leistungsfähigere Form der Generik in höherartigen Typen, die bei der Arbeit mit FP-orientierten Stapeln wie Typelevel sehr wichtig werden. Kotlin unterstützt diese nicht. Das ist einer der Gründe, warum wir uns entschieden haben, dass Scala-Code Kotlin-Code konsumieren soll, aber nicht umgekehrt.

Varianz bezieht sich auf die Art und Weise, wie wir generische Typen und Subtypen in Beziehung setzen. Wenn zum Beispiel Dog ein Subtyp von Mammal ist, bedeutet das, dass List ein Subtyp von List ist? Die Antwort hängt davon ab, wie wir die Dogs in List verwenden, und wird dem Compiler als Varianz-Anmerkungen mitgeteilt. Und hier kommt der Unterschied: In Java werden diese Annotationen ausschließlich in Methoden angegeben,

public static void pet(List<? extends Mammal> list) { ... }

während sowohl Scala als auch Kotlin Annotationen als Teil von Typen unterstützen,

interface List<out A>: Collection<A> { ... }

Sowohl Kotlin als auch Scala unterstützen die gleiche Funktion, aber die zugrunde liegende virtuelle Maschine nicht. Das bedeutet, dass beide Compiler ihren Quellcode in eine andere Form übersetzen müssen, plus einige zusätzliche Informationen, um die in den Java-Typen nicht kodierte Varianz wiederherzustellen. Leider werden diese zusätzlichen Informationen nicht in der gleichen Form gespeichert, so dass eine völlig vernünftige Kotlin-Typsignatur aus Sicht von Scala zu einem schrecklichen Durcheinander wird.

Zum Zeitpunkt des Schreibens haben wir keine andere Lösung gefunden, als explizit auf Typen mit Varianz auf der Scala-Seite zu casten. Hier geben wir zum Beispiel einen generischen Typ mit Varianz an,

formula.asInstanceOf[Formula[Info[_ <: Action, _ <: State, _ <: Response]]]

zu einem Argument, das in Kotlin mit dem Typ Formula<Info> deklariert wurde, wobei die Argumente auf Info die Varianz out haben. Die beste Lösung wäre natürlich, wenn sich die Compiler-Autoren von Scala und Kotlin auf eine Form von gemeinsamen Varianz-Annotationen einigen würden.

Kotlin-spezifische Funktionen

Die übrigen Probleme standen im Zusammenhang mit spezifischen Sprachmerkmalen von Kotlin. Wie im Fall der Generika erwies sich das Verständnis der Übersetzung von Kotlin in Java Bytecode in diesen Fällen als unglaublich nützlich.

Top-Level- und Erweiterungsfunktionen

In Kotlin können Sie eine Funktion (oder eine Eigenschaft) direkt in der Datei definieren, im Gegensatz zur Definition eines umgebenden Typs wie in Java oder Scala 2 (Scala 3 hat Top-Level-Definitionen).

fun pet(cuties: List<Mammal>) { ... }

Sie können noch einen Schritt weiter gehen und mit Hilfe von Erweiterungsfunktionen "vortäuschen", dass diese Funktion über einen bestehenden Typ deklariert wurde (dies wurde auch in Scala 3 hinzugefügt, wo zuvor implizite Definitionen erforderlich waren).

fun List<Mammal>.pet() { ... }

Wenn Sie diesen Kotlin-Code von Scala aus konsumieren, offenbaren die Funktionen wieder ihre wahre Form. In diesem Fall werden sie als statische Mitglieder einer neuen Klasse deklariert, die mit dem Namen des Typs definiert ist,

public class ExtensionsKt {
  public static void pet(List<? extends Mammal> cuties) { ... }
}

Glücklicherweise erlaubt Scala den Import aller statischen Mitglieder der Klasse in den Geltungsbereich. Das bedeutet, dass wir vortäuschen können, pet als Top-Level-Definition zu haben,

import ExtensionsKt._

Wie bei den Generika wäre es von Vorteil, wenn Kotlin und Scala (oder die gesamte JVM) eine gemeinsame Methode zur Deklaration eines Members als Top-Level unterstützen würden.

Koroutinen

Wenn es eine Funktion gibt, die Kotlin vom Rest der JVM-Sprachen abhebt, dann sind das definitiv Coroutines (obwohl meine Kollegen hart daran arbeiten, diesen Stil in Scala 3 einzuführen!) Die Deklaration einer Funktion als suspend erfordert nur minimale Änderungen im Kotlin-Code,

suspend fun List<Mammal>.pet() { ... }

hat aber tiefgreifende Auswirkungen auf die Art und Weise, wie der Code kompiliert wird. Ilmirus hat eine ausführliche Anleitung zu diesem Thema geschrieben, aber für unsere Zwecke müssen wir nur wissen, dass eine suspended Funktion nicht normal zurückkehrt. Stattdessen endet sie mit dem Aufruf einer Funktion, die als zusätzliches Argument übergeben wird. In unserem obigen Beispiel sieht der Code aus Sicht der JVM wie folgt aus.

public class ExtensionsKt {
  public static pet(List<? extends Mammal> cuties, Continuation<Unit> c) {
    ...
    // end by calling the continuation
    c.resume(Unit)
  }
}

Theoretisch sollte die Interaktion mit diesem Code nicht allzu schwierig sein. Wir müssen lediglich ein Continuation erstellen, um festzulegen, wie wir "zurückkehren" wollen. Da Continuation eine funktionale Schnittstelle ist, können wir dafür bekannte Lambda-Ausdrücke verwenden. Leider hat uns die Praxis einen Strich durch die Rechnung gemacht und jeder mögliche Weg, mit den Funktionen von suspendzu interagieren, endete auf IllegalStateException oder ClassCastException. Nach zu vielen erfolglosen Versuchen gaben wir uns geschlagen und erstellten fast doppelte Definitionen einiger der Kerntypen und -funktionen.

Der Abschied von einer unserer Lieblingsfunktionen in Kotlin hinterlässt bei uns einen bitteren Beigeschmack. Wir freuen uns auf Project Loom als gemeinsames Substrat für Coroutines in der gesamten JVM, wodurch die Geschichte hier vielleicht ein viel glücklicheres Ende nehmen wird.

Fazit

Wir sind mit dem aktuellen Stand der Interoperabilität zwischen Kotlin und Scala sehr zufrieden und setzen das Modell "zwei Sprachen unter einem Dach" in unserem Projekt fort. Es ist kein Geheimnis, dass wir, das funktionale Team bei Xebia, sowohl Scala als auch Kotlin lieben. Wenn Sie daran interessiert sind, mit uns zu sprechen, können Sie unser Kontaktformular verwenden oder unsere Schulungsplattform besuchen, um mehr über diese beiden Ökosysteme zu erfahren.

Verfasst von

Alejandro Serrano Mena

Contact

Let’s discuss how we can support your journey.