Das tatsächliche Muster ist wesentlich allgemeiner als nur der Datenzugriff. Es ist eine einfache Möglichkeit, eine domänenspezifische Sprache zu erstellen, mit der Sie einen AST erhalten, und dann einen oder mehrere Interpreter zu haben, um den AST auszuführen, wie Sie möchten.
Der kostenlose Monad-Teil ist nur ein praktischer Weg, um einen AST zu erhalten, den Sie mit Haskells Standard-Monad-Funktionen (wie Do-Notation) zusammenstellen können, ohne viel benutzerdefinierten Code schreiben zu müssen. Damit ist auch sichergestellt , dass Ihr DSL ist zusammensetzbare : Sie können es in Teilen definieren und setzen dann die Teile zusammen in einer strukturierten Art und Weise, so dass Sie die Vorteile von Haskell normalen Abstraktionen wie Funktionen übernehmen.
Wenn Sie eine freie Monade verwenden, erhalten Sie die Struktur einer zusammensetzbaren DSL. Sie müssen nur die Teile angeben. Sie schreiben einfach einen Datentyp, der alle Aktionen in Ihrem DSL umfasst. Diese Aktionen können alles tun, nicht nur den Datenzugriff. Wenn Sie jedoch alle Datenzugriffe als Aktionen angegeben haben, erhalten Sie einen AST, der alle Abfragen und Befehle für den Datenspeicher angibt. Sie können dies dann so interpretieren, wie Sie möchten: Führen Sie es in einer Live-Datenbank aus, führen Sie es in einem Schein aus, protokollieren Sie einfach die Befehle zum Debuggen oder optimieren Sie die Abfragen.
Schauen wir uns ein sehr einfaches Beispiel für einen Schlüsselwertspeicher an. Im Moment werden sowohl Schlüssel als auch Werte nur als Zeichenfolgen behandelt, aber Sie können mit ein wenig Aufwand Typen hinzufügen.
data DSL next = Get String (String -> next)
| Set String String next
| End
Mit dem next
Parameter können wir Aktionen kombinieren. Wir können dies benutzen, um ein Programm zu schreiben, das "foo" bekommt und "bar" mit diesem Wert setzt:
p1 = Get "foo" $ \ foo -> Set "bar" foo End
Leider reicht dies für ein sinnvolles DSL nicht aus. Da wir next
für die Komposition verwendet haben, hat der Typ p1
dieselbe Länge wie unser Programm (dh 3 Befehle):
p1 :: DSL (DSL (DSL next))
In diesem Beispiel ist die Verwendung next
dieser Methode etwas ungewöhnlich, aber es ist wichtig, dass unsere Aktionen unterschiedliche Typvariablen haben sollen. Wir möchten vielleicht eine getippte get
und set
zum Beispiel.
Beachten Sie, wie das next
Feld für jede Aktion unterschiedlich ist. Dies deutet darauf hin, dass wir damit DSL
einen Funktor erstellen können:
instance Functor DSL where
fmap f (Get name k) = Get name (f . k)
fmap f (Set name value next) = Set name value (f next)
fmap f End = End
Tatsächlich ist dies die einzig gültige Möglichkeit, einen Functor daraus zu machen, sodass wir deriving
die Instanz automatisch erstellen können, indem wir die DeriveFunctor
Erweiterung aktivieren .
Der nächste Schritt ist der Free
Typ selbst. Das ist es, was wir verwenden, um unsere AST- Struktur darzustellen , die auf dem DSL
Typ aufbaut. Sie können sich das wie eine Liste auf Typebene vorstellen, in der "cons" nur einen Funktor wie den DSL
folgenden verschachtelt :
-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a = Cons a (List a) | Nil
Damit können wir Free DSL next
Programmen unterschiedlicher Größe den gleichen Typ geben:
p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Welches hat den viel schöneren Typ:
p2 :: Free DSL a
Der eigentliche Ausdruck mit all seinen Konstruktoren ist jedoch immer noch sehr umständlich zu verwenden! Hier kommt der Monadenteil ins Spiel. Wie der Name "freie Monade" andeutet, Free
ist dies eine Monade - solange f
(in diesem Fall DSL
) ein Funktor ist:
instance Functor f => Monad (Free f) where
return = Return
Free a >>= f = Free (fmap (>>= f) a)
Return a >>= f = f a
Jetzt kommen wir voran: Wir können die do
Notation verwenden, um unsere DSL-Ausdrücke zu verbessern. Die Frage ist nur, worauf es ankommt next
. Nun, die Idee ist, die Free
Struktur für die Komposition zu verwenden, also werden wir einfach Return
für jedes nächste Feld setzen und die Do-Notation die ganze Installation erledigen lassen:
p3 = do foo <- Free (Get "foo" Return)
Free (Set "bar" foo (Return ()))
Free End
Das ist besser, aber immer noch etwas umständlich. Wir haben Free
und Return
überall. Glücklicherweise gibt es ein Muster , das wir ausnutzen können: die Art , wie wir „Lift“ ein DSL - Aktion in Free
immer der gleichen wir es in wickeln Free
und gelten Return
für next
:
liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)
Auf diese Weise können wir nun nette Versionen von jedem unserer Befehle schreiben und haben eine vollständige DSL:
get key = liftFree (Get key id)
set key value = liftFree (Set key value ())
end = liftFree End
Auf diese Weise können wir unser Programm schreiben:
p4 :: Free DSL a
p4 = do foo <- get "foo"
set "bar" foo
end
Der nette Trick ist, dass es zwar p4
wie ein kleines Imperativ-Programm aussieht, aber tatsächlich ein Ausdruck ist, der den Wert hat
Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Der freie Monadenteil des Musters hat uns also ein DSL beschert, das Syntaxbäume mit netter Syntax erzeugt. Wir können auch zusammensetzbare Teilbäume schreiben, indem wir nicht verwenden End
; Zum Beispiel könnten wir haben, follow
was einen Schlüssel nimmt, seinen Wert erhält und diesen dann selbst als Schlüssel verwendet:
follow :: String -> Free DSL String
follow key = do key' <- get key
get key'
Jetzt follow
kann in unseren Programmen genauso verwendet werden wie get
oder set
:
p5 = do foo <- follow "foo"
set "bar" foo
end
So bekommen wir auch eine schöne Komposition und Abstraktion für unser DSL.
Jetzt, wo wir einen Baum haben, kommen wir zur zweiten Hälfte des Musters: dem Interpreter. Wir können den Baum interpretieren, wie wir möchten, indem wir ihn mit Mustern abgleichen. Auf diese Weise können wir unter anderem Code für einen realen Datenspeicher schreiben IO
. Hier ist ein Beispiel für einen hypothetischen Datenspeicher:
runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
do res <- getKey key
runIO $ k res
runIO (Free (Set key value next)) =
do setKey key value
runIO next
runIO (Free End) = close
runIO (Return _) = return ()
Dies wertet gerne jedes DSL
Fragment aus, auch eines, mit dem es nicht endet end
. Glücklicherweise können wir eine "sichere" Version der Funktion erstellen, die nur Programme akzeptiert, die mit geschlossen end
werden, indem die Signatur des Eingabetyps auf gesetzt wird (forall a. Free DSL a) -> IO ()
. Während die alte Unterschrift akzeptiert einen Free DSL a
für jeden a
(wie Free DSL String
, Free DSL Int
und so weiter), ist diese Version akzeptiert nur eine , Free DSL a
die für Arbeiten jeden möglichen a
-Welche wir mit nur schaffen können end
. Dies garantiert, dass wir nicht vergessen, die Verbindung zu schließen, wenn wir fertig sind.
safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO
(Wir können nicht einfach mit runIO
diesem Typ beginnen, da er für unseren rekursiven Aufruf nicht richtig funktioniert. Wir könnten jedoch die Definition von runIO
in einen where
Block verschieben safeRunIO
und den gleichen Effekt erzielen, ohne beide Versionen der Funktion verfügbar zu machen.)
Das Einspielen unseres Codes IO
ist nicht das Einzige, was wir tun können. Zum Testen möchten wir es möglicherweise State Map
stattdessen mit einem reinen ausführen . Das Ausschreiben dieses Codes ist eine gute Übung.
Das ist also das freie Muster von Monade + Interpreter. Wir stellen ein DSL her und nutzen die freie Monadenstruktur, um die gesamte Installation zu erledigen. Mit unserem DSL können wir die Do-Notation und die Standard-Monadenfunktionen nutzen. Um es dann tatsächlich zu benutzen, müssen wir es irgendwie interpretieren; da der baum letztendlich nur eine datenstruktur ist, können wir ihn für verschiedene zwecke beliebig interpretieren.
Wenn wir dies verwenden, um Zugriffe auf einen externen Datenspeicher zu verwalten, ähnelt es in der Tat dem Repository-Muster. Es vermittelt zwischen unserem Datenspeicher und unserem Code und trennt die beiden. In mancher Hinsicht ist es jedoch spezifischer: Das "Repository" ist immer ein DSL mit einem expliziten AST, den wir dann verwenden können, wie wir möchten.
Das Muster selbst ist jedoch allgemeiner. Es kann für viele Dinge verwendet werden, die nicht unbedingt externe Datenbanken oder Speicher erfordern. Es ist überall dort sinnvoll, wo Sie die Feinsteuerung von Effekten oder mehreren Zielen für eine DSL wünschen.