MyOwnSafe - Funktionale Programmierung in der Praxis - Teil 2
Die Active Group hat die Webanwendung MyOwnSafe für die MyOwnSafe GmbH entwickelt. Der Anwender kann in dieser Anwendung Informationen und Dokumente zu seinen Versicherungen, seinem Vermögen und sonstige persönliche Informationen ablegen und pflegen, sowie Vorkehrungen treffen um diese Informationen im Todes- oder Krankheitsfall bestimmten Personen zugänglich zu machen.
In einem vorherigen Artikel habe ich die Gesamtarchitektur und die eingesetzen Technologien beschrieben. Dieser Artikel soll an einem kleinen Ausschnitt der Software zeigen, wie die funktionale Programmierung in OCaml die Entwicklung vereinfachen und damit auch beschleunigen konnte.
OCaml ist eine bereits seit über fünfzehn Jahren etablierte und gefestigte Variante aus der ML-Sprachfamilie. Sie unterstützt neben dem funktionalen Programmieren auch imperative und objekt-orientierte Paradigmen, ein statisches Typsystem mit besonders mächtiger Typinferenz, sowie ein gutes Modulsystem. Es gibt eine Vielzahl von Bibliotheken, insbesondere zur Web- und Netzwerkprogrammierung, und der Compiler erzeugt sehr performanten, nativen Code.
Die funktionale Sprache in Microsoft‘s Visual-Studio, F#, ist übrigens ein „Ableger“ von OCaml, und hat dementsprechend sehr viel Ähnlichkeit mit OCaml.
Hintergrund
Für die Speicherung von Kreditkarten- und Kontodaten der Kunden, und die Abbuchung von Beträgen über diese Bezahlverfahren, wurde ein externer Dienstleister in Anspruch genommen, und zwar die Firma Expercash. Da Expercash eine Schnittstelle anbietet, bei der MyOwnSafe niemals selbst Kenntnis der Kreditkarten-Daten erlangt, ist keine teure Zertifizierung durch die Kreditkarten-Unternehmen notwendig. Ein großer Kostenvorteil für ein Start-Up.
Diese Anbindung an den externen Zahlungsdienstleister ist es auch, die in diesem Artikel als Beispiel dienen soll.
Aufgabe
Um einen bestimmten Betrag vom Kunden einzuziehen, leitet die Client-Software den Kunden zunächst auf eine Seite von Expercash weiter, auf der er über eine SSL-verschlüsselte Verbindung seine Konto- oder Kreditkartendaten eingibt, und den Betrag autorisiert. Als Ergebnis davon erhält die MyOwnSafe-Software lediglich einen eindeutigen Schlüssel, den sogenannten Payment-Authorization-Key.
Die eigentliche Ausführung der Abbuchung erfolgt anschließend vom Server aus über einen HTTP-Request, bei dem einer der Parameter dieser Payment-Authorization-Key ist:
https://xyz.de/pay?key=authkey4711
Als Ergebnis erhält man eine XML-Struktur, aus der man noch ablesen muss, ob die Abbuchung erfolgreich war, oder welcher Fehlergrund vorliegt. Wenn sie erfolgreich war, erhält man außerdem noch eine sogenannte Payment-Id, mit der man den Abbuchungsvorgang später identifizieren kann. Die Struktur im Fehlerfall:
oder im Erfolgsfall:
Implementierung
Man benötigt also eine Funktion, die einen payment_key
nimmt, und
entweder zurückliefert dass ein Fehler mit einem bestimmten Fehlercode
passiert ist, oder dass der Aufruf erfolgreich war und eine bestimmte
payment_id
vergeben wurde.
Die möglichen Rückgabewerte bestehen aus einer festen Anzahl von Möglichkeiten (zwei) mit jeweils unterschiedlichen zugehörigen Daten. Für diese Aufzählungstypen gibt es in OCaml, wie in vielen funktionalen Programmiersprachen, eine sehr komfortable Unterstützung. Die Syntax dafür sieht folgendermaßen aus:
PaymentSuccess
und PaymentFailure
bezeichnet man dabei als
Konstruktoren für Werte vom Typ payment_result
, die in diesem Fall
jeweils einen String, beziehungsweise eine Zahl als Parameter erwarten.
Ebenso leicht kann man Typ-Aliase definieren, also alternative Namen für existierende Typen, wie zum Beispiel für den Parameter der Funktion:
Die Signatur der zu implementierenden Funktion ist damit also in OCaml-Syntax:
Zur Implementierung verwenden wir eine HTTP-Client-Bibliothek, sowie eine XML-Parser-Bibliothek. Zur Vereinfachung gehen wir davon aus, dass die HTTP-Client-Bibliothek folgende Schnittstelle bietet:
Zwei Typen mit einem Stern dazwischen definieren in OCaml den Typ von Tupeln aus Werten der jeweiligen Typen. Der Ausdruck für die Erzeugung von Tupeln besteht dann einfach aus zwei Ausdrücken getrennt durch ein Komma, wie wir weiter unten gleich sehen werden.
Zunächst noch zum XML-Parser, von dem wir vereinfachend annehmen, dass er folgende Typen und Funktionen anbietet:
Also einer Funktion die einen String nimmt und den darin kodierten
XML-Baum als Objekt vom Typ xml_node
zurückgibt. Der Typ hat
wiederum drei Konstruktoren, einen für Element-Knoten, einen für
Attribut-Knoten und einen für Text-Knoten. In einem Element-Knoten ist
dabei wiederum eine Liste von Knoten enthalten, d.h. xml_node
ist
rekursiv definiert.
Das besonders komfortable Feature von OCaml, das einem bei der nun
folgenden Implementierung der Funktion do_payment
hilft, ist das
sogenannte Pattern-Matching.
Bevor wir dazu kommen noch ein Wort zu Funktionen: let f p1 p2 = ...
definiert eine Funktion f
mit zwei Parametern, und der Ausruck f 1
2
ruft diese Funktion mit den beiden Zahlen als Argumente auf.
Und jetzt zur Implementierung von do_payment
:
Das Pattern-Matching selbst ist der Ausdruck match ... with ...
innerhalb der Funktion do_payment
. Er definiert eine sehr mächtige
Fallunterscheidung abhängig von dem Wert des Funktionsaufrufs von
http_get
mit dem Rückgabewert einer kleinen Hilfsfunktion ec_url
als Argument (die Definition folgt weiter unten). Die Tests sind
dabei jeweils auf der linken Seite den Pfeils ->
. Von dem ersten
Fall der passt wird der Ausdruck rechts vom Pfeil ausgewertet. Das
Pipe-Symbol |
trennt dabei zwei Fälle und kann als oder gelesen
werden.
Pattern-Matching leistet dabei viel mehr als ein „normales“ if, nämlich drei Dinge:
-
Dekonstruktion von Werten: In diesem Fall ist das Ergebnis von
http_get
ein Tupel. Mit der gleichen Syntax mit dem Tupel konstruiert werden, können sie innerhalb des Pattern-Matching wieder in die beiden Bestandteile, in diesem Fall eine Zahl und einen String, dekonstruiert werden. -
Vergleich mit konstanten Werten: Der Test des ersten Falls besteht zum Beispiel aus dem konstanten Ausdruck
200
. Dies bewirkt, dass der Test nur erfolgreich ist, wenn der von http_get zurückgegebene Status-Code gleich 200 ist. -
Bindung von Bestandteilen an neue Namen: Hier ist das der Rumpf der HTTP-Antwort, den wir in dem Test nicht genauer prüfen, sondern durch die Angabe eines Bezeichners (
body
) angeben, dass wir diesen Bestandteil des Tupels auf der rechten Seite dieses Falls noch weiter verwenden möchten; genauer als Argument für die Funktionparse_ec_body
die gleich folgt.
Letzter Fall eines Pattern-Matching ist wie hier häufig ein
else-Clause, der in OCaml durch eine sogenannte anonyme Bindung mit
dem Unterstrich angegeben werden kann. In diesem Fall wird hier eine
Exception mit failwith
ausgelöst.
Nun noch zu den fehlenden Hilfsfunktionen und der Funktion zum Analysieren der XML-Struktur, die ebenfalls von Pattern-Matching gebrauch macht:
Im ersten Pattern-Matching auf das Ergebnis von parse_xml body
sieht
man, dass Pattern-Matching nicht nur auf eingebauten Werten wie Tupeln
möglich ist, sondern, ohne weiteren Code schreiben zu müssen, auch auf
dem neu definierten Aufzählungstyp xml_node
. In diesen Fällen
zerlegt man die Werte häufig anhand der Konstruktoren des Typs; hier
interessieren uns allerdings nur Element-Knoten mit dem Namen
"payresult"
und deren Liste der Kind-Knoten.
Mit dieser Liste der Kind-Knoten führen wir ein map aus, d.h. wir
werten die Funktion xml_field
auf jeden einzelnen Kind-Knoten aus,
und sammeln die Ergebnisse in einer neuen Liste. Diese Liste wird dann
aus Tupeln bestehen, und ist damit eine sogenannte Assoziations-Liste,
eine einfachen Art von Dictionary. In dieser kann man mit der Funktion
assoc
aus dem Modul List
nach dem ersten Teil der Tupel suchen,
und erhält den zweiten Teil davon zurück.
In der Funktion xml_field
geschieht eine weitere Art von
Pattern-Matching, und zwar auf eine Liste von Werten. In diesem Fall
passt der erste Test nur, wenn node
ein Element-Knoten mit
beliebigem Namen (gebunden an tagname
) ist, und seine
Kindknoten-Liste aus einem einzigen Text-Knoten besteht, dessen Inhalt
nicht weiter getestet wird, aber an den Namen v
gebunden werden
soll.
Fazit
Das Zerlegen von Datenstrukturen, sowie das Überprüfen und Weiterverarbeiten einzelner Bestandteile, ist eine immer wiederkehrende häufige Aufgabe von Programmen bzw. deren Programmierern. In Sprachen ohne Pattern-Matching benötigt man für so eine Zerlegung in der Regel sehr viele Zeilen Code, mit entsprechnd hoher Wahrscheinlichkeit für Fehler. Pattern-Matching wie in Ocaml bietet demgegenüber eine extrem kurze, prägnante Syntax für dieses häufige Muster, und trägt damit besonders zur Beschleunigung der Entwicklung und Minimierung der Fehlerrate bei.
Die Active Group setzt daher auch weiterhin auf OCaml und andere mächtige funktionale Programmiersprachen, um robuste Programme so effizient wie möglich zu entwickeln.