Warum funktional?
Warum haben wir als Firma entschieden Softwareentwicklung fast ausschließlich in funktionalen Programmiersprachen durchzuführen? Dieses Blog gibt auf diese Fragen jede Woche eine neue Antwort. Und wir hoffen mit unseren Antworten Softwareentwickler und Manager von funktionaler Programmierung zu überzeugen.
Uns ist aber auch klar dass nicht in jedem Projekt eine funktionale Sprache zum Einsatz kommen kann, sei es aus politischen Gründen oder aufgrund externer Zwängen. So werden beispielsweise iOS-Apps typischerweise in Objective-C geschrieben und eine moderene Webanwendung wird mit großer Wahrscheinlichkeit in Javascript entwickelt.
Heute möchte ich eine Antwort auf die Frage „warum funktional?“ anhand eines Beispiels aus der Praxis geben. Das Beispiel stammt aus meiner alltäglichen Arbeit und zeigt, wie man auch in einer imperativen Sprache wie Objective-C durch funktionale Denkweise und die Prinzipien der funktionalen Programmierung einfacheren, besser wartbaren Code schreiben kann.
Warum also funktional?
Der für mich wichtigste Grund ist die Begrenztheit meiner geistigen Resourcen. Bei der Entwicklung eines Projekts oder Produkts gilt es viele Punkte im Auge zu behalten: Anforderungen des Kunden, Korrektheit, Performance, Usability und und und. Da bin ich sehr froh dass funktionale Programme typischerweise auf Seiteneffekte verzichten und ich daher über eine ganze Kategorien von weiteren Problemen gar nicht erst nachdenken muss.
Erst kürzlich ist mir wieder deutlich geworden, wieviel geistige Resourcen das Nachdenken über veränderbare Datenstrukturen, Zuweisungen oder Variablenaliasing kostet. Meine Aufgabe war es, für unsere iOS-App eine Komponente zu entwickeln, die grob gesagt beim Auftreten gewisser Ereignisse eine vorhandene Menge von IDs verändert und diese Änderung als Diff in einem Baum propagiert.
Ich hatte zunächst versucht, die Komponente mit Hilfe der in iOS verfügbaren Datenstrukturen zu lösen. Leider sind alle diese Datenstrukturen destruktiv, d.h. man muss höllisch aufpassen dass man nicht aus Versehen eine Datenstruktur ändert obwohl man zu einem späteren Zeitpunkt noch den alten Zustand benötigt. Beim Entwickeln habe ich dann gemerkt dass es relativ viel Zeit kostet über alle solche Auswirkungen einer destruktiven Operation nachzudenken, ohne dass man dabei dem eigentlichen Ziel wirklich näher kommt. Daher habe ich mich nach kurzer Zeit entschieden persistente Datenstrukturen zu verwenden. Solche Datenstrukturen sind aus der funktionalen Programmierung wohlbekannt und haben die schöne Eigenschaft, dass Änderungsoperationen nie die Datenstruktur selbst ändern, sondern eine neue, geänderte Sicht zurückliefern und die alte Datenstruktur unberührt lassen.
Als Beispiel möchte ich eine vereinfachte Version der Stelle innerhalb der
Komponente zeigen, an der das Diff zwischen der alten und der neuen Menge
von IDs berechnet wird. Unter dem Diff zweier Menge old
und new
verstehe ich dabei die Information, die nötig ist, um Änderungen
zwischen old
und new
im Baum propagieren zu können. In
meinem Anwendungsfall sind die Mengen selbst typischerweise relativ groß,
die Änderungen zwischen den Mengen aber ziemlich klein. Daher liegt es
nahe, das Diff zwischen old
und new
als ein Paar (toAdd, toRemove)
zu repräsentieren, so dass bei Anwenden des Diffs auf eine Menge s
die
Elemente aus toAdd
zu s
hinzugefügt werden, während die Elemente aus
toRemove
von s
entfernt werden.
Mein erster, destruktiver Ansatz sah in etwa so aus:
Wie in Objective-C üblich steht zwischen @interface
und @end
die
Deklarationen der öffentlichen Eigenschaften und Methoden der Klasse
DestructiveSetDiff
, während der @implementation
Block dann die
Methoden implementiert. Beachten Sie, dass in der mit (*)
markierten
Zeile ein Kopie der Menge new
erstellt wird. Dies ist nötig, um
Korrektheit sicherzustellen. Die beiden anderen Parameter old
und set
werden hingegen destruktiv modifiziert, zum einen aus Gründen der
Effizienz zum anderen einfach weil eine imperative Sprache wie Objective-C
diese Implementierung nahe legt. Eine Inspektion meines übrigen Codes
hatte mich zunächst davon überzeugt, dass diese destruktiven Modifikationen
in Ordnung sind.
Allerdings habe ich dann später noch Änderungen vorgenommen und plötzlich hatten die destruktiven Änderungen fatale Folgen, da die dort modifizierten Mengen an anderer Stelle mit Annahme des alten Zustands verwendet wurden. Konkret ist dieses Problem aufgetreten als ich eine Optimierung an der Serialisierung vorgenommen hatte.
Mit jedem Knoten im Baum sind nämlich mehrere Mengen von IDs assoziert,
die Auswahl der richtigen Menge geschieht über einen Schlüssel
vom Typ NSString
.
Vor der Optimierung hatte ich einfach alle mit einem Knoten assozierten
Mengen serialisiert und auf Platte geschrieben. Da es aber auch durchaus
vorkommen kann, dass sich gewisse Menge nicht ändern, wollte ich diesen
Umstand ausnützen und nur die veränderten Mengen speichern. Dazu habe ich
eine Klasse SerializableNodeData
eingeführt, der man explizit mitteilen
muss, dass sich eine Menge geändert hat, und die bei der
Serialisierung nur die tatsächlich geänderten Mengen speichert. Die
Schnittstelle der Klasse sieht in etwa so aus:
Die Implementierung von setValue:forKey:
hat folgende Form, wobei
self.dirtyKeys
die Menge der Schlüssel repräsentiert, deren Werte verändert wurden,
und self.values
die aktuellen Werte speichert.
Bei der Serialisierung werden dann nur die Schlüssel gespeichert die
in self.dirtyKeys
enthalten sind. Mein Code zum Anwenden des Diffs und zum
Speichern des neuen Werts sah dann nach der Optimierung in etwa so aus:
In dieser kompakten Darstellung und mit etwas Abstand ist offensichtlich was hier schiefgeht:
keiner der Schlüssel von SerializableNodeData
wird jemals als „dirty“ markiert werden,
denn der Vergleich [set isEqualToSet:old]
in der Methode setValue:forKey:
liefert immer true
da set
und old
Aliase für dasselbe Objekt sind. Daher passierte mit meiner
Optimierung bei der Serialisierung gar nichts.
Nach dieser Einsicht habe ich mich dann an meine funktionalen Wurzeln erinnert und eine
persistente Version PersistentSetDiff
geschrieben. In dieser Version
werden keine Parameter destruktiv modifiziert und die Methode applyDiff:
liefert ihr Ergebnis als Rückgabewert. Damit war der oben geschilderte Bug auch sofort
behoben.
Sie sehen außerdem, dass ich statt NSSet
und NSMutableSet
nun überall
eine selbstgeschriebene Klasse PersistentSet
verwende. Das Interface
von PersistentSet
sieht wie folgt aus:
Momentan ist die Implementierung von PersistentSet
naiv und basiert auf
einem NSMutableSet
, das an den richtigen Stellen kopiert wird. Falls es
mit der naiven Implementierung irgendwann mal Performanceprobleme geben
sollte, könnte ich die naive Implementierung durch eine deutlich
performantere ersetzen. Dazu bieten sich z.B. „Hash Tries“ an, wie sie
beispielsweise auch in Clojure
(Blogartikel)
oder Scala
(Sourcecode)
zum Einsatz kommen.
Neben PersistentSetDiff
habe ich bei der Implementierung meiner
Komponente auch noch an vielen anderen Stellen persistente Datenstrukturen
anstatt destruktiver verwendet. Dadurch konnte ich die Komponente
schneller entwickeln, da weniger Nachdenken über mögliche Auswirkungen
destruktiver Operationen nötig war. Außerdem wurde der Code dadurch besser
wartbar und weniger fehleranfällig.
So, das war‘s für heute mit meiner ganz persönlichen Einschätzung warum es sich lohnt in die funktionale Programmierung reinzuschauen. Ich freue mich auf ihr Feedback und ihre Fragen!