Dependency Injection in der funktionalen Programmierung

Seite 2: DSL, Interpreter

Inhaltsverzeichnis

Es stellt sich die Frage, wie eine API aussehen sollte, die das Zusammenspiel von get und put explizit gestaltet und gleichzeitig Dependency Injection für die Implementierung der Datenbanktabelle ermöglicht. In der funktionalen Programmierung ist die Antwort häufig eine eingebettete, domänenspezifische Sprache (Domain-Specific Language, DSL). Um einen derartigen Ansatz im beschriebenen Szenario zu nutzen, sind zunächst aus den Funktionen get und put Klassen eines Datentyps zu erzeugen:

type t =
Put of key * value
| Get of key

Der Datentyp t enthält die Werteklassen Put und Get. Put hat zwei Felder: Schlüssel vom Typ key und Wert vom Typ value. Get hat jeweils ein Feld, den Schlüssel vom Typ key. Damit lassen sich einzelne Befehle als Datenobjekte schreiben:

Put ("foo", 12)
Get "foo"

Allerdings gehören zu einer DSL nicht nur Befehle, sondern auch Programme – also deren Verkettungen. Sie sind nötig, da in Programmen anders als in reinen Listen die Argumente eines Befehls vom Ergebnis eines vorhergehenden abhängen können:

put "foo" ((get "foo") + 1)

Daher ist nun der Ergebnisfluss der Datenbankoperationen zu modellieren. In einem ersten Schritt sollten Entwickler jede Befehlsklasse um ein zusätzliches Feld erweitern, in dem der Befehl steht, mit dem es weitergeht:

type t =
Put of key * value * t
| Get of key * t

Um ein Programm zu beenden, ist ebenfalls ein Befehl nötig:

| Done

Jetzt ist es immerhin möglich, zu schreiben:

Put ("foo", 23, Put ("bar", 42, Done))

Das Programm, das ein Datenbankfeld inkrementiert, funktioniert allerdings immer noch nicht, weil eine Option fehlt, um das Resultat eines Get zu verarbeiten.

Um sie zu ergänzen, dient der in der funktionalen Programmierung oft benutzte Trick, statt eines Restprogramms vom Typ t eine Funktion zu benutzen, die den Wert entgegennimmt und abhängig davon das übrige Programm generiert:

| Get of key * (value -> t)

Damit lässt sich jetzt ein DSL-Programm entsprechend dem vorletzten Beispiel schreiben:

Get ("foo", fun r -> Put ("foo", r + 1, Done))

(fun ist die OCaml-Notation fĂĽr Lambda-AusdrĂĽcke. Bei der Funktion im Get handelt es sich um eine sogenannte Continuation.)

Bisher ist das Programm nicht in der Lage, Resultate nach außen zu geben. Sie kommen lediglich als zusätzliches Feld zu Done. Damit die Ergebnisse einen beliebigen Typ haben können, benötigt t einen Typ-Parameter. Die fertige Definition sieht wie folgt aus:

type 'a t =
Put of key * value * 'a t
| Get of key * (value -> 'a t)
| Done of '

(Typparameter werden in OCaml immer mit Apostroph geschrieben; in Java entspräche würde 'a t als t<'a> geschrieben.)

Dadurch lässt sich im obigen Beispielprogramm auch auf den alten Wert von "foo" zugreifen:

Get ("foo", fun r -> Put ("foo", r + 1, Done r))

Damit etwas passiert, fehlt noch ein Interpreter fĂĽr das Programm:

let rec run_with_hashtable (ht: (key, value) Hashtbl.t) (p: 'a t): 'a =
match p with
| Put (k, v, next) ->
begin
Hashtbl.add ht k v;
run_with_hashtable ht next
end
| Get (k, cont) ->
run_with_hashtable ht (cont (Hashtbl.find ht k))
| Done v -> v

Die Funktion run_with_hashtable erhält die Hash-Tabelle ht und ein Programm p vom Typ 'a t, führt Letzteres aus und liefert ein Resultat vom Typ 'a. Da das Programm p zu einer der drei Klassen Put, Get und Done gehört, verzweigt run_with_hashtable mit dem Konstrukt match entsprechend der drei Fälle. Dieses sogenannte Pattern Matching liest sich so:

  • Falls p ein Put mit SchlĂĽssel k, Wert v und Restprogramm next ist, wird zunächst der Eintrag zur Hashtabelle hinzugefĂĽgt und dann das Restprogramm ausgefĂĽhrt. (Das rec nach dem let ist in OCaml nötig, damit der rekursive Aufruf funktioniert.)
  • Wenn p ein Get mit SchlĂĽssel k und Continuation cont ist, ist zunächst der Datenbankeintrag mit Hashtbl.find nachzuschlagen und an cont zu ĂĽbergeben. Im Anschluss wird das Restprogramm ausgefĂĽhrt, das cont geliefert hat.
  • Wenn p den Wert Done hat, liefert das Programm den daran hängenden Wert.

So liefert etwa das folgende Programm 23:

run_with_hashtable (Hashtbl.create 0) (Put ("foo", 23, Get ↲
("foo", fun r -> Done r)))