Ist Kotlin das erfolgreichere Scala?
Kotlin und Scala sind Sprachen auf der JVM, die ähnliche Konzepte verfolgen. Dennoch wird Kotlin in der öffentlichen Wahrnehmung deutlich positiver aufgenommen als Scala. Im Rahmen dieses Artikels werden drei Thesen vorgestellt und erläutert, wie diese zum Erfolg einer Programmiersprache beitragen können.
Scala erlebte mit dem Big-Data-Hype im Jahre 2015 einen immensen Aufschwung. Frameworks wie Akka, Play, Spark, Lagom und Kafka waren komplett in Scala geschrieben oder boten primär eine Scala-API an. Die Programmiersprache füllte mit vielen innovativen Sprachkonzepten aus der funktionalen Programmierung eine Lücke, die Java seit Jahren nicht adressierte. Erst mit dem Erscheinen von Java 8 wurden Lambda-Ausdrücke, Closures und Funktionen als First-Class-Citizen auch auf der JVM der breiten Masse bekannt. Dies sind alles Konzepte, die der Scala-Entwickler bereits seit Jahren kennt und schätzt. Von anderen Konzepten wie Pattern-Matching, Type-Classes, For-Comprehensions und Extension-Methods können Java-Entwickler auch heute noch träumen, sind sie doch die Sprachkonstrukte, wofür Scala-Entwickler ihre Sprache lieben.
Doch wieso ist Scala dann nicht die vorherrschende Programmiersprache auf der JVM, zumindest für neue Projekte?
Tatsächlich überwiegt eine ganz andere Meinung zu Scala: Der Code ist unnötig kompliziert, unverständlich und schwer lesbar. Es wird zu viel konzeptuelles Vorwissen benötigt. Das sogenannte "Mindset" ist zu unterschiedlich. Scheinbar wird der Einstieg in Scala als ungewöhnlich aufwändig angesehen, ganz im Gegensatz zu Kotlin, einer von JetBrains entworfenen und entwickelten Sprache, die in den letzten Jahren einen kometenhaften Erfolg verzeichnete.
Bei vielen beliebten Programmiersprachen ist ein immer wiederkehrendes Muster erkennbar. Sprachen wie Java, Python oder C# erfordern nur eine sehr flache Lernkurve. Hierdurch verkürzt sich auch die Zeit, bis Entwickler produktiv mit der Sprache arbeiten können. Aber kann dies tatsächlich der einzige Grund für den Erfolg sein?
Die nachfolgenden Thesen kontrastieren Kotlin und Scala auf verschiedenen – vorwiegend nicht-technischen – Ebenen und geben eine Erklärung an die Hand, wieso die eine oder die andere Sprache trotz eines ähnlich ausgereiften technischen Unterbaus einen deutlich größeren Erfolg verzeichnet.
Leichte Zugänglichkeit und angenehme Lernkurve
Wenn eine Programmiersprache nicht unmittelbar nachvollziehbar ist, ist sie für den Mainstream nicht geeignet.
JetBrains wählte bei Kotlin einen sehr intelligenten Mix zwischen bekannten Pfaden und modernen Konzepten. Es ist immer noch eine objektorientierte Sprache, nur angereichert mit vielen Konzepten, von denen Java-Entwickler viele Jahre nur träumen konnten. Im Gegensatz zu Scala benötigt Kotlin kein anderes "Mindset", keine ungewohnte Denkweise, wie Probleme typischerweise in der Sprache gelöst werden. Der Softwaretechniker spricht von idiomatischer Programmierung, d. h. Code auf eine Weise zu verfassen, wie erfahrene Programmierer der Sprache sie auch verwenden würden. Diese Denkweise bzw. Mentalität, an Probleme heranzugehen, unterscheidet sich zwischen Kotlin und Java nicht fundamental. Für Java-Entwickler fühlt sich Kotlin sofort vertraut an und sie sind in der Lage, sehr schnell produktiv zu arbeiten. Ein weiterer Vorteil besteht darin, dass Java und Kotlin problemlos Seite an Seite in einem Projekt verwendet werden können. Ein Design-Ziel von Kotlin war eine möglichst reibungslose Interoperabilität mit Java. Erreicht wird dies unter anderem durch Platform-Types [1] und intelligente (Collection-)Type-Mappings [2]. Zusätzlich lösen Nullable-Types [3] das Problem mit Null-Referenzen auf eine konsistente und typsichere Weise, ohne auf das allgemeinere Konzept eines Option/Maybe-Types [4] zurückgreifen zu müssen.
Scala hingegen erfordert von vornherein bereits erheblich mehr Vorwissen oder stellt den Entwickler zumindest vor die Aufgabe, sich dieses Wissen anzueignen. Einerseits ist das Typsystem erheblich reicher. Während bei Java mit Generics und Varianz bereits die Grenze des Typsystems erreicht ist, kennt Scala zahlreiche weitere Konzepte, die Aussagen über Typen auch völlig unabhängig von einer Klasse erlauben. Der unter Scala-Entwicklern sehr bekannte Artikel "Scala’s Types of Types" [5] gibt einen recht guten Überblick über die Möglichkeiten des Typsystems der Sprache. Andererseits integriert Scala als selbsternannte objektfunktionale Programmiersprache viele Konzepte aus der funktionalen Programmierung. Während viele Java-Entwickler wohl erst 2014 mit dem Release von Java 8 erste Erfahrungen mit Lambdas, Funktionstypen und Higher-Order-Functions sammelten, war dies für Scala-Entwickler bereits lange Jahre gewohntes Handwerkszeug. Scalas objektfunktionales Programmierparadigma lässt sehr viele Freiheiten, die Segen und Fluch zugleich sein können. Der Programmcode kann rein objektorientiert, vorwiegend funktional oder auch eine Kombination von beidem sein. Die Syntax ermöglicht es, den gleichen Code auf viele verschiedene Weisen zu schreiben. Dies ermöglicht natürlich eine ungewohnte Flexibilität bei der Ausgestaltung des Codes. Allerdings erfordert dies vom Leser auch eine Vertrautheit mit viel mehr Konzepten und der Sprache im Allgemeinen. Ein weiteres Problem von Scala ist, dass Konzepte wie symbolische Methodennamen und Implicits das sogenannte "Code-Reasoning" erschweren. Code-Reasoning bezeichnet die Fähigkeit, die Funktionsweise und Wirkung einer bestimmten Codezeile nachzuvollziehen. Dem menschlichen Verstand fällt es leichter, lokale Effekte und Aktionen zu korrelieren. Daher ist folgender Code-Block auf den ersten Blick verwirrend:
Listing 1:
def times(value: Int): Int = value * 2
val doubled: Int = times(21)
val alsoDoubled: Int = times("21")
Die times-Methode erwartet einen Int-Parameter und liefert nach der Berechnung auch wieder ein Int zurück. Dennoch lässt sich die Methode in diesem Beispiel auch problemlos mit einem String-Parameter aufrufen. Dies liegt daran, dass durch ein entsprechendes Import-Statement folgende implizite Konvertierung von String nach Int in den Kontext geholt wird:
Listing 2:
implicit def str2Int(str: String): Int = Integer.parseInt(str)
Hierdurch wird der Aufruf der times-Methode mit einem String-Parameter möglich, da erst die implizite Konvertierung aufgelöst und erst danach die times-Methode aufgerufen wird. Es reicht in diesem Beispiel nicht mehr, ausschließlich die aufrufende Seite und die Methodenimplementierung zu betrachten. Vielmehr ist es notwendig, auch Import-Statements und mögliche Implicits zu bedenken.
Implicits ermöglichen ungeahnte Flexibilität, allerdings sind sie zugleich gefährlich und für die Lesbarkeit des Programmcodes abträglich, wenn sie nicht behutsam eingesetzt werden. Denn wenn eines die Code-Lesbarkeit erschwert, dann ist es Code, der das Prinzip der geringsten Überraschung (Principle of least astonishment[6]) verletzt.
Scala bietet wirklich keinen leichten Einstieg in die Sprache. Die Lernkurve ist ungewohnt steil. Die Menge an neuen Konzepten erfordert ein erhebliches Zeitinvestment. Dieses Investment wird auch für eine idiomatische Verwendung der Sprache vorausgesetzt, frei nach dem Motto "Take it or leave it!". Kotlin hingegen fühlt sich zu jedem Zeitpunkt vertraut an. Die Sprache führt neue moderne Konzepte ein, erwartet allerdings keine völlig neue Denkweise. Wer in Java programmieren kann, dem fällt ein Umstieg auf Kotlin nicht schwer.
Die erste These dieses Artikels lautet daher: Eine leichte Zugänglichkeit und eine angenehme Lernkurve lockt neue Entwickler über die Probierphase hinaus an.
Interoperabilität mit existierenden Ökosystemen
Wenn die Sprache es erlaubt, in gewohnten Pfaden zu wandeln, ist der halbe Weg zum Erfolg bereits beschritten.
Null-Sicherheit bei plattformübergreifenden Aufrufen
Kotlin wurde mit einem Fokus auf Interoperabilität mit dem existierenden Java-Ökosystem entwickelt. Es stellt kein Problem dar, Java-Code aus Kotlin oder umgekehrt aufzurufen. Kotlin kennt grundsätzlich zwei Klassen von Typen: Non-Nullable-Typen T sowie Nullable-Typen T?. Allerdings kann jeglicher Referenztyp in Java den Wert null annehmen und der Kotlin-Compiler kann im Allgemeinen nicht im Vorhinein ermitteln, ob eine Variable oder das Ergebnis eines Methodenaufrufs null ist oder nicht. Kotlin betrachtet daher jegliche Referenztypen, die aus einem Aufruf gegen Java-Code resultieren, als sogenannte Platform-Types. Diese bilden die zusätzliche synthetische Typklasse T!, die auf T oder T? abgebildet wird. Synthetisch bedeutet hier, dass keine Syntax in Kotlin existiert, um beispielsweise einen Wert String! zu deklarieren ("non-denotable type"). Beim Aufruf einer Java-Methode aus Kotlin heraus ist die wohl sicherste Herangehensweise, den Typ explizit als nullable zu deklarieren:
Listing 3:
// Typ wird explizit als nullable String deklariert
val value: String? = javaObject.getValue()
Der Compiler erzwingt vor der Verwendung der Variable eine Behandlung eines möglichen null-Wertes, beispielsweise durch den Safe-Call-Operator[7].
Listing 4:
// Typ wird explizit als non-nullable String deklariert
val value: String = javaObject.getValue()
Der Rückgabetyp kann allerdings auch explizit als non-nullable deklariert werden (Listing 4), wenn anderweitig sichergestellt ist, dass der Wert niemals null annehmen kann. Dies kann entweder im Methodenkontrakt so vorgegeben sein oder die Java-Methode wurde mit einer entsprechenden Nullability-Annotation deklariert, die vom Kotlin-Compiler unterstützt wird [8]. Falls der Rückgabewert dennoch null annehmen sollte, wird direkt bei der Zuweisung eine Exception geworfen. Dies erleichtert späteres Debugging, da die Exception nicht erst bei der ersten Verwendung auftritt.
Listing 5:
// ohne explizite Typangabe versucht der Compiler, den Typ automatisch zu ermitteln
val value = javaObject.getValue()
Die unsicherste Variante besteht darin, den Typ durch den Compiler inferieren zu lassen. Hierdurch wird keine Exception bei der Zuweisung geworfen, selbst wenn der Wert null sein sollte, sondern erst bei der ersten Verwendung. Der Compiler stellt allerdings sicher, dass ein Platform-Type nicht als Parameter einer Funktion übergeben werden kann, die einen Non-Nullable-Type erwartet. Der Compiler wird bei der Zuweisung zudem eine Warnung ausgeben: "Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable." Die Empfehlung ist daher, entweder den Typ explizit zu deklarieren oder, sofern möglich, eine der Nullability-Annotationen im Java-Code zu verwenden.
Scala hingegen unterscheidet nicht zwischen Nullable- und Non-Nullable-Typen. Der Compiler hindert den Entwickler nicht daran, eine Methode auf einem Wert aufzurufen, der aus einem Aufruf einer Java-Methode resultiert. Entwickler müssen selbst daran denken, dass eine Null-Behandlung notwendig sein könnte. Dies kann im Eifer des Gefechts schnell vergessen werden und nur ausgiebiges Testen leistet hier Abhilfe.
Die idiomatische Vorgehensweise für eine solche plattformübergreifende Null-Behandlung lautet in Scala, den Rückgabewert direkt in dem Option-Typ zu kapseln. Der Option-Typ ist mehr oder weniger mit der Optional-Klasse aus Java 8 vergleichbar. An diesem Beispiel lässt sich auch die unterschiedliche Lösungsorientierung zwischen Scala und Java verdeutlichen. Während dies in Scala die idiomatische Lösung darstellt, gilt die Herangehensweise in Java weithin als Anti-Pattern, da viele kurzlebige Objekte erzeugt werden. In funktionalen Sprachen ist die Verwendung solcher Container-Typen zur typsicheren Kapselung und Erhöhung des Abstraktionsniveaus jedoch allgemein üblich.
Inkompatible Collection-Frameworks
Die Interoperabilität zwischen Scala und Java wird außerdem durch die inkompatiblen Collection-Frameworks erschwert. Scala besitzt ein äußerst mächtiges Collection-Framework, welches jedoch nicht auf dem bekannten Java-Collection-Interfaces basiert. Bei einem plattformübergreifenden Methodenaufruf ist es daher grundsätzlich nötig, eine explizite Konvertierung der Collection-Instanz vorzunehmen:
Listing 6:
import scala.collection.JavaConverters._
val scalaList: List[String] = List("23", "42")
val value: mutable.Buffer[String] =
javaObject.withList(scalaList.asJava).asScala
Die Scala-Standardbibliothek bietet zwar die Konvertierungsmethoden asJava und asScala an, aber Entwickler müssen diese explizit aufrufen. Das Ergebnis dieses Aufrufs ist hier übrigens eine veränderliche Liste (mutable.Buffer), obwohl die Eingabedatenstruktur eine unveränderliche Liste (immutable.List) war. Aus Performancegründen wird die Java-Liste intern in einer speziellen Scala-Buffer-Implementierung gekapselt, wodurch kein Kopieren der Elemente notwendig ist.
Kotlin löst das gleiche Problem durch intelligentes Type-Mapping [9] auf Compiler-Ebene:
Listing 7:
val kotlinList: List<String> = listOf("23", "42")
val value: MutableList<String> = javaObject.withList(kotlinList)
Kotlin muss zwar auch auf eine MutableList zurückgreifen, da Java-Listen standardmäßig veränderlich sind. Allerdings entfällt die explizite Konvertierung der Collection-Klassen, wie sie in Scala notwendig ist.
Frameworks vs. Libraries
Spätestens beim Thema Frameworks unterscheiden sich Scala und Kotlin fundamental. Scala fokussiert sich weniger auf Frameworks, sondern viel mehr auf Bibliotheken. Frameworks unterscheiden sich dahingehend von Bibliotheken, dass die Struktur des Programms und die Kontrolle des Programmflusses durch das Framework vorgegeben wird. Bei Bibliotheken rufen Entwickler die Funktionen selbst innerhalb des Codes auf. Diese Kontrollumkehr bei den Frameworks wird als Inversion of Control bezeichnet [10].
Scala wurde mit dem Ziel entwickelt, eine mächtige Sprache bereitzustellen, die alleine durch Spracheigenschaften viele Probleme löst, die in anderen Sprachen durch Frameworks übernommen werden. Dependency-Injection kann in Scala beispielsweise durch das Typsystem bereits zur Compile-Zeit aufgelöst werden. Dies wird durch den Einsatz von Self-Types im Rahmen des Cake-Patterns erreicht [11]. Eine andere Alternative ist beispielsweise der Einsatz von Scala-Macros. Diese Funktionalität erlaubt eine direkte Programmierung gegen den abstrakten Syntaxbaum zur Compile-Zeit. Die wohl bekannteste Implementierung für Scala, die diese Funktionalität nutzt, ist die MacWire-Bibliothek [12].
Diese Fokussierung auf Bibliotheken führt allerdings zu einem Effekt, der die Interoperabilität mit existierenden Java-Frameworks behindert. Während Kotlin beispielsweise eine wirklich hervorragende Integration des Spring-Frameworks bereitstellt, ist der Einsatz von Spring in Scala sehr gewöhnungsbedürftig [13]. Die Implementierung eines kleinen Microservices inklusive REST-Controller, Service-Schicht und Datenbankpersistenz unterscheidet sich in vielen kleinen Details, die erst langwierig über praktisches Ausprobieren herausgefunden werden müssen. Und letztlich fühlt sich die Implementierung nie so an, als würden Spring und Scala wirklich harmonieren. Die Implementierung der Datenbankpersistenz ist durch den Einsatz von JPA (Java-Persistence-API) an die Java-Bean-Konvention und den Einsatz von Collections aus dem java.util-Paket gebunden. Entity-Klassen sind hierdurch zwangsläufig veränderlich und widersprechen der grundsätzlichen Empfehlung, in Scala nur unveränderliche Datenstrukturen zu verwenden. Zudem werden stetige explizite Konvertierungen zwischen den Scala- und den Java-Collection-Klassen notwendig.
Dies führt zu folgendem Effekt, den Carlo Jelmini in seinem Blog-Artikel "Java interoperability: Kotlin vs Scala" beschreibt [14]: "Scala was not designed with Java interoperability as a primary goal: it’s simply a byproduct of running on the JVM. Even though interoperability is used as a marketing argument to attract developers, in practice when moving to Scala, teams have to leave behind the Java ecosystem and adopt the Scala ecosystem in order to be productive."
Scala bildet durch den bibliotheksorientierten Ansatz ein völlig eigenes Ökosystem. Eine wirkliche Interoperabilität zwischen Scala und Java ist anders als in Kotlin nicht gegeben. Wenn Entwickler jedoch vor der Entscheidung stehen, jahrelang angesammeltes Wissen hinter sich lassen zu müssen, ist Widerstand durchaus nachvollziehbar. Selbst wenn hierfür auf manch modernes Konzept und Sprachmittel verzichtet werden muss.
Die zweite These folgt aus dieser Schlussfolgerung: Eine Interoperabilität mit dem existierenden Ökosystem einer Sprache fördert den Entwicklerzugang immens.
Ausgeprägtes Entwickler-Tooling
Bei den ersten Schritten in einer neuen Sprache entscheiden nicht die großartigen Sprachmittel, sondern wie angenehm einem der Einstieg gemacht wird.
Nur die wenigsten Entwickler werden heutzutage noch ohne eine IDE programmieren. Sie nehmen Entwicklern so viel Arbeit ab. Dies mag zwar nicht für eine Minderheit gelten, die weiterhin Vi oder Emacs zu schätzen weiß, aber sicherlich für den größten Teil der Entwickler. Als Scala im Jahr 2004 veröffentlicht wurde, existierte nur ein Compiler auf der Kommandozeile. Mehr Entwickler-Tooling wurde nicht angeboten. Die Scala-Core-Entwickler haben ihren Code bevorzugt in Emacs geschrieben. Eine ernsthafte IDE-Unterstützung war anfangs kein primäres Ziel. Es dauerte bis zum 20. Dezember 2011, bis eine robuste und stabile Scala-IDE für Eclipse veröffentlicht wurde. Heutige Scala-Entwickler können sich glücklich schätzen, dass Lightbend (damals Typesafe) – das Unternehmen hinter Scala – nach der Gründung im Jahr 2011 den Fokus auf die Entwicklung eines richtigen IDE-Supports legte. Denn seitdem hat sich das Entwickler-Tooling erheblich gebessert. Das Scala-Plugin für JetBrains’ IntelliJ-IDEA ist sehr ausgereift. Allerdings kann nur die Verwendung von Eclipse und IntelliJ für die Entwicklung mit Scala empfohlen werden. Das Scala-Plugin für Netbeans weist auch heute noch viele Schwächen und Fehler auf, die den produktiven Einsatz erschweren.
Kotlin hingegen bietet ein völlig anderes Bild. Hinter der Entwicklung von Kotlin steckt JetBrains, das Unternehmen, welches auch die IntelliJ-IDEA entwickelt; die IDE mit dem größten Marktanteil für Sprachen auf der JVM. Für JetBrains stand bei der Entwicklung der Sprache eine gute Tool-Unterstützung stets im Fokus. Der Grund hierfür ist ebenso einfach wie einleuchtend. Kotlin sollte die Verkaufszahlen ihrer Enterprise-IDE vorantreiben [15]. Dies ist ohne jeden Zweifel eine eigennützige Intention. Doch wenn sich Entwickler im Gegenzug eben nicht regelmäßig mit den Unzulänglichkeiten und Fehlern der IDE-Plugins für die Sprache beschäftigen müssen, ist die Entwicklungserfahrung gleich eine völlig andere. Entwickler können produktiver arbeiten und sich besser auf das Wesentliche konzentrieren.
Kotlins Erfolg resultiert natürlich nicht alleine aus diesem Faktor. Als Google 2017 ankündigte, dass die direkte Entwicklung gegen die Android-Platform mit Kotlin unterstützt werden würde, trug dies selbstverständlich massiv zum Erfolg bei. Nur zwei Jahre später sollte Kotlin sogar die bevorzugte Sprache für die Android-Platform werden. Nichtsdestotrotz machte Kotlin von Beginn an einiges besser als Scala. Und die Erfahrung zeigt, dass Entwickler einer Sprache selten eine zweite Chance geben, wenn die Meinung erst gefestigt ist. Daher ist solcher verlorener Boden nur schwer wieder gutzumachen.
Für diesen Abschnitt lautet die These daher: Eine gute Tool-Unterstützung von Beginn an ist heutzutage eine Voraussetzung für langfristigen Erfolg.
Fazit
Eine angenehme Lernkurve bewirkt breitere Akzeptanz und ist ein nicht zu unterschätzender Erfolgsfaktor. Scala wird oft als konzeptionell zu überladen angesehen. Die Sprache bietet eine ungeahnte Flexibilität, stellt den Entwickler allerdings auch vor größere Herausforderungen. Auch die Denkweisen zur Problemlösung unterscheiden sich zwischen Kotlin und Scala. Während Kotlin auf dem Sprachspektrum mehr den objektorientierten Sprachen zugeordnet werden kann, befindet sich Scala eher auf der anderen Seite des Spektrums bei den funktionalen Sprachen. Dass Scala von Haskell beeinflusst wurde, ist deutlich spürbar. Auch bei der Interoperabilität mit Java liegt Kotlin deutlich vorne. Nullable-Types und Platform-Types gestalten die Zusammenarbeit von Kotlin und Java angenehm und größtenteils null-sicher. Die Type-Mappings von Collection-Klassen auf Compiler-Ebene nehmen dem Entwickler unnötige explizite Konvertierungen zwischen den beiden Welten ab. Die Verwendung von Java-Frameworks aus Kotlin heraus gestaltet sich fast immer problemlos. Scala dagegen profitiert massiv davon, sich tatsächlich komplett im Scala-Ökosystem zu bewegen, da erst dann die meisten Vorzüge der Sprache zur Geltung kommen. Beim Thema Tooling-Support liegen Kotlin und Scala mittlerweile gleichauf. Die Fehler der Vergangenheit sind allerdings schwer zu kompensieren, wenn die Meinung erst gefestigt wurde. Daher ist der Erfolg von Kotlin nicht überraschend, auch wenn Scala wahrscheinlich seinen Nischenmarkt im Big-Data- und Event-Streaming-Sektor behalten wird.
- Kotlin: Null-safety and platform types
- Kotlin: Mapped types
- Kotlin: Null safety
- Wikipedia: Option type
- Scala’s Types of Types
- Wikipedia: Principle of least astonishment
- Kotlin: Safe calls
- Kotlin: Nullability annotations
- Kotlin: Mapped types
- Wikipedia: Inversion of control
- M. Odersky, M. Zenger: Scalable Component Abstractions, OOPSLA’05
- Github: Add dependency information to readme
- Spring: Spring Framework
- C. Jelmini: Java interoperability: Kotlin vs Scala
- D. Jemerov: Why JetBrains needs Kotlin