it-swarm.com.de

Was ist das Muster "Free Monad + Interpreter"?

Ich habe Leute gesehen, die über Free Monad with Interpreter gesprochen haben, insbesondere im Zusammenhang mit dem Datenzugriff. Was ist das für ein Muster? Wann möchte ich es vielleicht benutzen? Wie funktioniert es und wie würde ich es implementieren?

Ich verstehe (aus Posts wie this ), dass es darum geht, das Modell vom Datenzugriff zu trennen. Wie unterscheidet es sich vom bekannten Repository-Muster? Sie scheinen die gleiche Motivation zu haben.

96

Das tatsächliche Muster ist tatsächlich wesentlich allgemeiner als nur der Datenzugriff. Es ist eine einfache Methode, eine domänenspezifische Sprache zu erstellen, die Ihnen einen AST gibt, und dann einen oder mehrere Interpreter zu haben, um das AST, wie Sie möchten, "auszuführen".

Der kostenlose Monadenteil ist nur eine praktische Möglichkeit, ein AST] zu erhalten, das Sie mithilfe der Standard-Monadenfunktionen von Haskell (wie der Do-Notation) zusammenstellen können, ohne viel benutzerdefinierten Code schreiben zu müssen. Dies stellt auch sicher, dass Ihr DSL ist zusammensetzbar : Sie können es in Teilen definieren und dann die Teile strukturiert zusammenfügen, sodass Sie die normalen Abstraktionen wie Funktionen von Haskell nutzen können .

Wenn Sie eine freie Monade verwenden, erhalten Sie die Struktur einer zusammensetzbaren DSL. Alles was Sie tun müssen, ist die Teile anzugeben. 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 Ihre Datenzugriffe als Aktionen angegeben haben, erhalten Sie ein AST, das alle Abfragen und Befehle an den Datenspeicher angibt. Sie können dies dann so interpretieren, wie Sie möchten: Führen Sie es aus Führen Sie eine Live-Datenbank aus, führen Sie sie gegen einen Mock aus, protokollieren Sie einfach die Befehle zum Debuggen oder versuchen Sie sogar, die Abfragen zu optimieren.

Schauen wir uns ein sehr einfaches Beispiel für einen Schlüsselwertspeicher an. Im Moment behandeln wir nur Schlüssel und Werte als Zeichenfolgen, 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 Parameter next können wir Aktionen kombinieren. Wir können dies verwenden, um ein Programm zu schreiben, das "foo" erhält und "bar" mit diesem Wert setzt:

p1 = Get "foo" $ \ foo -> Set "bar" foo End

Leider reicht dies für eine sinnvolle DSL nicht aus. Da wir next für die Komposition verwendet haben, hat der Typ von p1 Die gleiche Länge wie unser Programm (dh 3 Befehle):

p1 :: DSL (DSL (DSL next))

In diesem Beispiel scheint die Verwendung von next wie folgt etwas seltsam, aber es ist wichtig, wenn unsere Aktionen unterschiedliche Typvariablen haben sollen. Möglicherweise möchten wir beispielsweise einen typisierten get und set.

Beachten Sie, dass das Feld next für jede Aktion unterschiedlich ist. Dies deutet darauf hin, dass wir damit DSL zu einem Funktor machen 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 einzige gültige Methode, um daraus einen Functor zu machen. Daher können wir deriving verwenden, um die Instanz automatisch zu erstellen, indem wir die Erweiterung DeriveFunctor aktivieren.

Der nächste Schritt ist der Typ Free. Das ist es, was wir verwenden, um unsere AST Struktur ) darzustellen, die auf dem Typ DSL aufbaut. Sie können sich das vorstellen wie eine Liste auf der Ebene des Typs , in der "cons" nur einen Funktor wie DSL verschachtelt:

-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a   = Cons a (List a)     | Nil

Wir können also Free DSL next Verwenden, um Programmen unterschiedlicher Größe dieselben Typen zu 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, ist Free 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 weiter: Wir können die Notation do verwenden, um unsere DSL-Ausdrücke schöner zu machen. Die Frage ist nur, was für next eingegeben werden soll. Nun, die Idee ist, die Struktur Free für die Komposition zu verwenden, also setzen wir einfach Return für jedes nächste Feld und lassen die Do-Notation die gesamte Installation ausführen:

p3 = do foo <- Free (Get "foo" Return)
        Free (Set "bar" foo (Return ()))
        Free End

Das ist besser, aber es ist immer noch etwas umständlich. Wir haben überall Free und Return. Glücklicherweise gibt es ein Muster, das wir ausnutzen können: Die Art und Weise, wie wir eine DSL-Aktion in Free "heben", ist immer dieselbe - wir verpacken sie in Free und wenden Return für next an:

liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)

Auf diese Weise können wir jetzt schöne Versionen jedes 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 folgendermaßen schreiben:

p4 :: Free DSL a
p4 = do foo <- get "foo"
        set "bar" foo
        end

Der nette Trick ist, dass p4 Wie ein kleines zwingendes 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 Nice-Syntax erzeugt. Wir können auch zusammensetzbare Unterbäume schreiben, indem wir End nicht verwenden. Zum Beispiel könnten wir follow haben, das einen Schlüssel nimmt, seinen Wert erhält und diesen dann als Schlüssel selbst verwendet:

follow :: String -> Free DSL String
follow key = do key' <- get key
                get key'

Jetzt kann follow in unseren Programmen genauso verwendet werden wie get oder set:

p5 = do foo <- follow "foo"
        set "bar" foo
        end

So erhalten wir auch für unser DSL eine schöne Komposition und Abstraktion.

Jetzt, wo wir einen Baum haben, kommen wir zur zweiten Hälfte des Musters: dem Interpreter. Wir können den Baum nach Belieben interpretieren, indem wir ihn mit Mustern abgleichen. Dies würde es uns ermöglichen, Code für einen realen Datenspeicher in IO und andere Dinge zu schreiben. Hier ist ein Beispiel gegen 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 ()

Dadurch wird jedes DSL-Fragment glücklich ausgewertet, auch eines, das nicht mit end endet. Glücklicherweise können wir eine "sichere" Version der Funktion erstellen, die nur Programme akzeptiert, die mit end geschlossen wurden, indem die Signatur des Eingabetyps auf (forall a. Free DSL a) -> IO () Gesetzt wird. Während die alte Signatur einen Free DSL a Für jeden a (wie Free DSL String, Free DSL Int Und so akzeptiert on) akzeptiert diese Version nur einen Free DSL a, der für jeden möglichen a funktioniert, den wir nur mit end erstellen können. 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 damit beginnen, runIO diesen Typ zu geben, da dies für unseren rekursiven Aufruf nicht ordnungsgemäß funktioniert. Wir könnten jedoch die Definition von runIO in einen where -Block in safeRunIO verschieben und den gleichen Effekt erzielen, ohne beide Versionen des Funktion.)

Das Ausführen unseres Codes in IO ist nicht das einzige, was wir tun können. Zum Testen möchten wir es möglicherweise stattdessen mit einem reinen State Map Ausführen. Das Ausschreiben dieses Codes ist eine gute Übung.

Das ist also das freie Monaden + Interpreter-Muster. Wir stellen ein DSL her und nutzen die freie Monadenstruktur, um alle Installationen durchzuführen. Wir können die Do-Notation und die Standard-Monadenfunktionen mit unserem DSL verwenden. 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 nach Belieben für verschiedene Zwecke interpretieren.

Wenn wir dies verwenden, um Zugriffe auf einen externen Datenspeicher zu verwalten, ähnelt es tatsächlich dem Repository-Muster. Es vermittelt zwischen unserem Datenspeicher und unserem Code und trennt die beiden voneinander. In mancher Hinsicht ist es jedoch spezifischer: Das "Repository" ist immer ein DSL mit einem expliziten AST, das 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 beinhalten. Es ist überall dort sinnvoll, wo Sie die Feinsteuerung von Effekten oder mehreren Zielen für ein DSL wünschen.

138
Tikhon Jelvis

Eine freie Monade ist im Grunde eine Monade, die eine Datenstruktur in der gleichen "Form" wie die Berechnung erstellt, anstatt etwas Komplizierteres zu tun. ( Es gibt Beispiele, die online zu finden sind. ) Diese Datenstruktur wird dann an einen Code übergeben, der sie verbraucht und die Operationen ausführt. * Ich bin mit dem Repository-Muster nicht ganz vertraut, aber von was ich gelesen habe es scheint eine übergeordnete Architektur zu sein, und ein kostenloser Monad + Interpreter könnte verwendet werden, um sie zu implementieren. Andererseits könnte der freie Monad + -Interpreter auch verwendet werden, um ganz andere Dinge wie Parser zu implementieren.

* Es ist erwähnenswert, dass dieses Muster nicht nur für Monaden gilt und tatsächlich mit kostenlosen Anwendungen effizienteren Code erzeugen kann, oder freie Pfeile . ( Parser sind ein weiteres Beispiel dafür. )

15
Dan