Als Student in den 90ern kam ich das erste Mal mit funktionaler Programmierung in Berührung, und es tat sich eine faszinierende Welt auf. Alleine die Begriffe klangen magisch: Anonyme Funktionen, Tail-Recursion, Higher-Order-Functions. Lazy Evaluation erlaubte unendlich lange Listen, und Typen wurden inferiert.
Gleichzeitig dominierten in der Arbeitswelt C++ und Java, und mir wurde schnell klar, dass ich in der Praxis der funktionalen Programmierung kaum begegnen werde.
Sprung nach 2025: Nun arbeite ich seit sieben Jahren in Projekten, die funktionale Sprachen ganz pragmatisch einsetzen. Wie kommt diese erstaunliche Renaissance, und was bedeutet das für den Programmieralltag?
Horizontale Skalierung #
Ein Konzept, das auf den ersten Blick nichts mit funktionaler Programmierung zu tun hat, ist Horizontale Skalierung: Von einer Anwendung werden mehrere gleichartige Instanzen nebeneinandergestellt, die den Dienst gleichberechtigt erbringen. Das Konzept hat sich durchgesetzt, denn neben besagter Skalierung ermöglicht es auch eine höhere Ausfallsicherheit: Wenn eine Komponente nicht läuft, sind die anderen Instanzen noch verfügbar. Auch Rolling Deployments ohne Downtime werden dadurch möglich.
Spannend wird es, wenn ich in meiner Anwendung Zustand habe, mir also zum Beispiel die Aktionen eines Nutzers merke. Wenn jetzt eine andere Instanz den Dienst übernimmt, kennt diese den Zustand nicht. Ich erinnere mich, dass in den Anfangstagen hier für Tomcat Worker Sticky sessions in Mode waren, der Nutzer verband sich technisch immer mit derselben Instanz der Anwendung, die dann den Zustand hielt.
Deutlich robuster wurde es, als der Zustand der Anwendung in eine Datenbank verlagert wurde: Hier landet eine Anfrage in einer zufällig ausgewählten Instanz, und diese liest und speichert ihren Zustand in einer Datenbank, die von allen Instanzen geteilt wird.
Michael Sperber formulierte es in einem Seminar bei uns so: “Der Zustand der Anwendung wird an den Rand der Anwendung gedrängt”. Und damit sind wir plötzlich ganz nahe bei der Funktionalen Programmierung.
Auftritt funktionale Programmierung #
Foto © Fabian Nilius
Keinen Zustand zu halten, ist ein Kern funktionaler Programmierung. Wenn wir also schon Anwendungen haben, die keinen Zustand halten, ist der Weg zu funktionalem Code nicht weit. Wie kommen wir dahin? Und was wären die Vorteile davon?
Der Übergang zu funktionaler Programmierung wird erleichtert durch Sprachen wie Kotlin oder Scala, die interoperabel mit Java sind und sowohl funktionale als imperative Konzepte unterstützen. Im Kotlin-Code sieht der Unterschied zwischen imperativ und funktional so aus:
Ein imperatives Eis, der Zustand wird mehrmals geändert.
var eis = Eis()
eis.neueKugel("Schokolade")
eis.neueKugel("Birne")
eis.mitStreusel("Haselnuss")
Ein funktionales Eis ändert nicht seinen Zustand, sondern wir bekommen bei jedem Aufruf eine geänderte Kopie zurück:
val eis = Eis()
.neueKugel("Schokolade")
.neueKugel("Birne")
.mitStreusel("Haselnuss")
Das ist fluent-interface-Stil, wie man ihn in Java von Buildern oder Streams kennt, bei dem die Aufrufe hintereinander gehängt werden können.
Erleichtert wird das durch Kotlins data class und Scalas case class, die eine mächtige und gleichzeitig einfache Grundlange bieten, um funktionale Klassen zu bauen. Wie im obigen Eis-Beispiel sind diese Klassen typischerweise immutable und bieten eine copy/clone-Methode, um eine geänderte Kopie zu erzeugen.
Seiteneffekte machen das Lebens kompliziert #
Was aber bringt der Umstieg?
Für uns ist funktionaler Code oft einfacher zu verstehen, weil er weniger Überraschungen enthält. Gerade in Legacy-Code gibt es immer wieder Seiteneffekte, in denen Instanzen an überraschenden Stellen modifiziert werden, der Code wird dadurch schwieriger verständlich und schwerer änderbar.
Betrachten wir folgendes Stück Java-Code aus dem Apache Tika Projekt:
void parse(InputStream stream,
ContentHandler handler,
Metadata metadata,
ParseContext context)
Es ist aus dem Schnipsel nicht ersichtlich, welche der Parameter in der Methode eventuell im parse geändert wird, tatsächlich modifizieren einige Parser den ParseContext.
In einer funktionalen Welt ist der Effekt offensichtlich: Wenn eine Methode etwas ändern will, muss sie eine entsprechende Kopie an den Aufrufer zurückgeben. So würde diese Funktion einen aktualisierten ParseContext zurückgeben. Wir sehen an der Signatur in der funktionalen Welt sofort, welche Effekte die Funktion hat.
Multithreading entschärft #
Da ein Backend typischerweise viele Anfragen gleichzeitig bearbeitet, ist Multithreading allgegenwärtig, gleichzeitig ist einem Stück Code nicht einfach anzusehen, wo es von mehreren Threads durchlaufen wird.
Multithreading-Bugs entstehen, wenn ein Zustand aus verschiedenen Threads gleichzeitig bearbeitet wird. Dies sind schwer analysierbare und kaum reproduzierbare Bugs: Oft sind die typischen Tests hier grün, und der Fehler tritt erst bei bestimmten Abläufen in der Produktion auf.
In der funktionalen Welt ist dies aber ein viel kleineres Problem, weil es keine Zustände gibt, auf die die Threads zugreifen.
Hindernisse in der Praxis #
Reicht es für die Umstellung, einfach alle var zu val zu ändern? Nach unserer Erfahrung ist die Datenbankanbindung oft eine Herausforderung auf dem Weg zur funktionalen Programmierung. Gerade JPA/Hibernate setzt auf modifizierbare Entities und ist nicht für die Nutzung mit immutable (val) data classes vorgesehen. Tatsächlich werden Änderungen an Entities vom DB-Framework im Hintergrund bis in die Datenbank persistiert — Seiteneffekt par excellence.
Für eine klare funktionale Codebasis will man zu anderen Datenbank-Libraries wechseln. Ähnlich zu spring data jpa ist spring data jdbc, das funktionale Entities unterstützt – allerdings eigene Konzepte und Herausforderungen hat.
This is where the magic happens #
Noch einmal zusammengefasst: Insbesondere durch Horizontale Skalierung verbreiten sich Anwendungen, die keinen fachlichen Zustand halten. Hier machen es hybride Sprachen wie Scala und Kotlin leicht, in die funktionale Welt zu wechseln, Datenbank-Anbindungen können ein Hindernis sein. Durch den Verzicht auf Zustände macht Funktionale Programmierung aus unserer Sicht den Code einfacher.
Es gibt also sehr weltliche, pragmatische Gründe, heute funktionale Programmierung zu verwenden. Was aber ist mit der Magie? Wenn man etwas genauer hinschaut, gibt es diese immer noch: Effekt-Systeme, Monaden, Type-level programming, Kombinatoren — es wartet eine faszinierende Welt.
Kommentare