Mit Kotlin gibt es seit 2016 eine zu Java kompatible Sprache, in der imperativ programmiert werden kann, aber deren Vorzüge sich vor allem im durchdachten Design funktionaler Konzepte zeigen.
Wo kommt Kotlin her und in welcher Beziehung steht es zu Java?
Kotlin wurde 2011 erstmals von dem in St. Petersburg ansässigen Unternehmen JetBrains vorgestellt und 2016 in der Version 1.0 veröffentlicht. Die Sprache ist voll kompatibel zu Java, setzt auf die JVM auf und kann einfach in bestehende Projekte integriert werden. Seit 2017 unterstützt Google die Entwicklung von Android-Anwendungen in Kotlin und bevorzugt die Sprache seit 2019 für die Plattform [Heise]. Mit Kotlin können ebenfalls Anwendung für iOS entwickelt werden. Kotlin Multiplatform Mobile (KMM) erlaubt dabei die parallele Entwicklung für beide Betriebssysteme.
Stand heute steht Kotlin laut TIOBE-Index 1 auf Platz 28 der populärsten Programmiersprachen. Im PYPL-Index 2 belegt die Sprache den 12. Platz.
Mit Frameworks wie Ktor (Web-Server/-Client) [Ktor], Koin (Dependency Injection) [Koin], Kotest (Testing) [Kotest] und vielen weiteren gibt es hier gute Werkzeuge, Produktivsysteme auch von Grund auf nativ in Kotlin aufzuziehen.
1) TIOBE zählt die gefundenen Ergebnisse zu Programmiersprachen aus verschiedenen Suchmaschinen.
2) PYPL nutzt Google Trends für Tutorial-Suchanfragen.
Grundgedanken der funktionalen Programmierung
Um sich den Vorteilen der funktionalen Programmierung zu nähern, sollte sich zuerst mit dem Grundgedanken des Paradigmas beschäftigt werden.
Allgemein
Der Grundgedanke stammt, wie es der Name erwarten lässt, aus der Mathematik. Statt einzelne Operationen auf Objekten auszuführen und diese dadurch sukzessive zu manipulieren, führen wir Funktionen aus, die Eingabewerte zu Ausgabewerten transformieren; vergleichbar dem aus der Schule bekannten f(x) = y.
Anders gesagt: Statt sich mental auf das „Wie“ zu versteifen, abstrahiert die funktionale Programmierung von dieser Ebene und zwingt den Entwickler, mehr über das „Was“ nachzudenken. In der Praxis bedeutet dies weiterhin, dass Daten verändert werden müssen, wie später noch an Beispielen verdeutlicht wird. Allerdings ändert sich die Art, Code zu lesen, und das kann dabei helfen, sich mehr auf den Kern eines Problems konzentrieren zu können, statt den Großteil der Arbeit mit dem eigentlichen Umsetzungsprozess verbringen zu müssen.
Immutabilität, Scope und Nebeneffekte
Um bei der Metapher einer mathematischen Funktion zu bleiben, es würde niemand für f(x) = x + 2 und f(2) erwarten, dass am Ende etwas anderes als 2 + 2 = 4 herauskommt (unter vereinfachten Annahmen, liebe Mathematiker). Die Erwartungshaltung in der Programmierung unterscheidet sich in der Regel nicht davon und das Ziel ist es, Nebeneffekte und unvorhergesehenes Verhalten soweit es geht zu vermeiden.
Das Verhalten einer Anwendung soll ideal deterministisch sein. Um das zu erreichen, sollten alle Werte, die zum Ausführen einer Funktion benötigt werden, als feste Parameter von außen mitgegeben und diese nach Möglichkeit nicht auf Objekte angewiesen sein, über deren Datenzustand zur Ausführungszeit keine zuverlässige Aussage getroffen werden kann. Listing 1 bis 3 zeigen Beispiele.
Listing 1: Mögliche Nebeneffekte in Java durch mehrfachen lesenden Zugriff auf eine Member-Variable. Die Lesbarkeit ist durch einen größeren Kontext zusätzlich erschwert (member ist außerhalb des Scopes)
Listing 2: Die potenziellen Nebeneffekte bei Nebenläufigkeit wurden entfernt
Listing 3: Komplette Kapselung der Methode von außen. Es wird nur auf lokale Werte zugegriffen und der aktualisierte Wert von member wird mit dem Ergebnis nach außen zurückgegeben
Parallelität, Wartbarkeit und null-Sicherheit
Die Reduktion von Nebeneffekten hat dafür einen anderen positiven „Nebeneffekt”, die Nebenläufigkeit von Anwendungen betreffend. Wenn die Funktionen in sich gekapselt ausgeführt werden können, wird so der Parallelisierungsprozess erleichtert. Gerade in der heutigen Zeit, in der Hardware eher horizontal als vertikal skaliert wird, ist es wichtig, ohne großen Aufwand Software schreiben zu können, die sich bei gleichzeitiger Ausführung nicht in die Quere kommt.
Damit einhergehen auch die erhöhte Wartbarkeit und Lesbarkeit von Code. Wenn Funktionen isoliert betrachtet werden können, verringert dies den kognitiven Aufwand, der geleistet werden muss, um zu verstehen, was ihre jeweiligen Aufgaben sind. Und das ist gut.
Etwas, das Kotlin (nicht als erste Sprache) unterstützt, ist eine standardmäßige null-Sicherheit von Werten (siehe Kasten). Außer, wenn es explizit – und das sollte die Ausnahme sein – anders erwartet wird, müssen Variablen einen Wert ihrem Typ entsprechend besitzen. Wenn dieses System nicht ausgenutzt wird, zwingt es dazu, die Anwendung mit einer Ausfallsicherheit zu entwerfen, die Standardwerte vorsieht und den Programmfluss dabei möglichst nicht durch Exceptions oder unschöne Konstrukte durchbricht. Das erhöht die Wartbarkeit und Lesbarkeit des Quellcodes ungemein. Listing 4 und 5 zeigen Beispiele.
Listing 4: Überprüfung in Java, ob ein Parameter null ist mit anschließender Verarbeitung
Listing 5: Direkte Verarbeitung von Parametern durch null-Sicherheit in Kotlin
Die Grundfunktionen der funktionalen Programmierung
Nach diesem kurzen Überblick über einige der wichtigsten Grundelemente der funktionalen Programmierung soll auf die Frage eingegangen werden, die sich an diesem Punkt bestimmt schon dem einen oder anderen gestellt hat: „Wie sieht funktionale Programmierung denn nun aus?“ Gezeigt werden soll dies in Kotlin anhand der drei wichtigsten Funktionen des Paradigmas: map, reduce und filter.
map-Funktion Vor einem imperativen Hintergrund entspricht die map-Funktion einer for-Schleife, in deren Rumpf beliebige Funktionen ausgeführt und zu einer neuen Liste verarbeitet werden. Soll beispielsweise eine Liste aller Preise von Produkten erstellt werden, muss in Java manuell über die Originalliste iteriert, die Preise extrahiert und diese in eine neue Liste einfügt werden.
Mit der map-Funktion wird eine Funktion übergeben, die die Abbildungsvorschrift f(x),x -> y definiert und eine Liste vom Rückgabetyp der Funktion generiert. Dies kann wie in dem Beispiel in Listing 6 aussehen, in dem eine List<Product> zu einer List<Double> transformiert wird.
Listing 6: Beispiel einer map-Funktion, das eine List<Product> zu einer List<Double> transformiert
filter-Funktion Filter ist eine Funktion, die in Java einer for-Schleife entspricht, in deren Rumpf Boolesche Ausdrücke ausgewertet und Elemente, die diese erfüllen, zu einer neuen Liste zusammengefügt werden. Im folgenden Beispiel einer List<Product> werden alle Produkte herausgefiltert, die einen Preis >= 0,5 besitzen. Das Ergebnis entspricht demnach einer List<Product>, für die alle enthaltenen Elemente einen Preis <0,5 besitzen:
val cheapProductList = productList.filter{product->
product.getConsumerPrice()<0.5}
reduce-Funktion Reduce reduziert eine Menge X von Elementen auf einen Ausgabewert y mithilfe einer Reduzierungsfunktion. In Java ist dafür über eine Liste zu iterieren, und dabei werden Operationen ausgeführt, die die einzelnen Elemente zu einem separaten Ergebnisobjekt hinzufügen. Ein einfaches Beispiel ist dabei das Aufsummieren von Einzelpreisen zu einer Gesamtsumme:
sumAllPrices = priceList.reduce {sum, price->sum+price}
Funktionen höherer Ordnung
Denkt man an frühere Java-Versionen zurück, sah eine vergleichbare, flexible Problemlösung mitunter so aus, dass eine anonyme Klasse erstellt, eine Methode überschrieben und diese dann in der umgebenden Methode genutzt wurde.
Möglich macht die vereinfachte Schreibweise in Listing 7 in Kotlin und auch in Java seit Version 8 der Einsatz von anonymen Funktionen, die als Parameter an Funktionen wie map weitergereicht werden können. Funktionen, die andere Funktionen als Eingabewerte akzeptieren, werden Funktionen höherer Ordnung genannt. In Java nennt man sie gemeinhin Lambda-Ausdrücke, in Anlehnung an das Lambda-Kalkül, welches in den 30er Jahren erdacht wurde und, um hier den Bogen zu spannen, einen wesentlichen Einfluss auf die Entwicklung des funktionalen Programmierparadigmas hatte.
Listing 7: Einsatz von anonymen Funktionen als Parameter
Was Java-Entwickler von Kotlin lernen können
Das Fachgebiet der funktionalen Programmierung ist viel größer, als es hier angerissen werden kann, und bietet noch viele weitere spannende Gedankengänge. Das Lambda-Kalkül als tiefergreifendes Thema und Einfluss für die funktionale Programmierung, oder auch weitere Designelemente von Kotlin, in denen die Sprache von ihren Vorgängern gelernt hat, seien hier beispielhaft genannt.
Abgesehen von den Themen, die hier grob behandelt wurden, gibt es aber natürlich auch Nachteile und Argumente, die gegen den Einsatz von funktionaler Programmierung sprechen. Gerade auch die Implikationen zur Performanz von funktionalen Programmiersprachen im Vergleich zu imperativen Sprachen in Bezug auf ihren Abstraktionsgrad ist ein spannendes Thema. Es gibt abseits davon auch andere gute Gründe und Anwendungsfälle, in denen es sinnvoller ist, nicht in diesem Stil zu programmieren, auf die hier nicht im Detail eingegangen werden kann.
Funktionale Programmierung kann jedoch die Wartbarkeit und Nebenläufigkeit von Quellcode erhöhen. Im Schaffensprozess hilft die Abstraktion von einzelnen Operationen hin zu Funktionen, besser mit domänenspezifischen Problemen umzugehen. In vielen Fällen arbeiten Entwicklerinnen und Entwickler auf Mengen von Eingabedaten, auf denen jeweils die gleichen Transformationen ausgeführt und Ausgabemengen erstellt werden sollen; und wenn das so ist, dann lohnt sich der Gedanke, ob man das nicht lieber funktional lösen möchte.
Weitere Informationen
[Hau17] Idiomatic Kotlin. Best Practices, https://phauer.com/2017/idiomatic-kotlin-best-practices/#functionalprogramming
[Heise] Google I/O: Googles Bekenntnis zu Kotlin, https://www.heise.de/developer/meldung/I-O-2019-Googles-Bekenntnis-zu-Kotlin-4417060.html
[infoq] https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/
[Koin] https://insert-koin.io/
[Kotest] https://kotest.io/
[Kotlin] Kotlin-Dokumentation, https://kotlinlang.org/docs/home.html
[Ktor] https://ktor.io/