2013-10-05 17 views
9

Ich habe eine Anwendungsarchitektur, in der Benutzereingaben zu einigen Automaten fließen, die im Kontext des Ereignisstroms ausgeführt werden und den Benutzer zu verschiedenen Teilen der Anwendung leiten. Jeder Teil der Anwendung kann aufgrund von Benutzereingaben Aktionen ausführen. Zwei Teile der Anwendung teilen jedoch einen bestimmten Zustand und lesen und schreiben konzeptionell in denselben Zustand. Der Nachteil ist, dass die zwei "Threads" nicht gleichzeitig laufen, einer von ihnen ist "pausiert", während der andere "Ausgänge" "ausgibt". Was ist die kanonische Methode, um diese Berechnung der Zustandsverteilung zu beschreiben, ohne auf eine globale Variable zurückzugreifen? Macht es Sinn, dass die beiden "Threads" lokale Zustände, die durch irgendeine Form der Nachrichtenübermittlung synchronisiert werden, behalten, obwohl sie auf keinen Fall gleich sind?Wie teilen Sie in der funktionalen reaktiven Programmierung den Status zwischen zwei Teilen der Anwendung?

Es gibt kein Codebeispiel, da die Frage konzeptioneller ist, aber Antworten mit Beispielen in Haskell (unter Verwendung eines beliebigen FRP-Frameworks) oder einer anderen Sprache sind willkommen.

+1

Ich denke, dass diese Frage zu breit ist, um eine spezifische Antwort zu geben. Jede der vorgeschlagenen Strategien (Synchronisierung, FRP, globale Variablen) kann für die gegebene Situation geeignet sein. Oder möglicherweise eine lokal geteilte "IORef" oder "MVar". Oder wenn die Berechnungen wirklich in einem einzigen Thread sind, ein 'StateT'-Monadetransformator. Es ist mir nicht klar, ob "" threads "tatsächliche Threads bedeutet, die von' forkIO' erstellt wurden, oder wenn sie streng konzeptuell sind und Sie nur einen Thread ausführen. –

+2

@JohnL: Da diese Frage FRP erwähnt, denke ich eine Antwort darüber, wie ein Verhalten oder Ereignisstrom durch eine Anwendung zu teilen wäre gut. Ich denke, ein Verhalten (oder mehr als eines, falls zutreffend) durch die Anwendung zu führen, ist in etwa eine gute Wahl, aber ich müsste wirklich die Details ausarbeiten, bevor ich daraus eine Antwort mache. Vielleicht, wenn in ein paar Stunden niemand kommt ... –

Antwort

13

Ich habe an einer Lösung für dieses Problem gearbeitet. Die High-Level-Zusammenfassung ist, dass man:

A) Destillieren all gleichzeitig Code in eine reine und Single-Threaded-Spezifikation

B) Die Single-Threaded-Spezifikation verwendet StateT

Die gemeinsamen Staat zu teilen Die Gesamtarchitektur wurde vom Model-View-Controller inspiriert. Sie haben:

  • Controller, die
  • Ansichten effekt Eingänge sind, die
  • Ein Modell effekt Ausgänge sind, die einen reinen Strom Transformation

Das Modell mit einem Controller in Wechselwirkung treten kann nur und eine Ansicht. Beide Controller und Ansichten sind jedoch Monoide, sodass Sie mehrere Controller zu einem einzigen Controller und mehrere Ansichten zu einer einzigen Ansicht kombinieren können. Tisch, sieht es wie folgt aus:

controller1 -           -> view1 
       \          /
controller2 ---> controllerTotal -> model -> viewTotal---> view2 
      /          \ 
controller3 -           -> view3 

        \______ ______/ \__ __/ \___ ___/ 
         v    v   v 
        Effectful  Pure Effectful 

Das Modell ist ein reines, Single-Threaded-Strom-Transformator, der Arrow und ArrowChoice implementiert. der Grund dafür ist, dass:

  • Arrow ist die Single-Thread entspricht Parallelität
  • ArrowChoice ist die Single-Thread entspricht Gleichzeitigkeit

In diesem Fall verwende ich push-basierten pipes, die scheinen, eine korrekte Arrow und ArrowChoice Instanz zu haben, obwohl ich noch an der Überprüfung der Gesetze arbeite, so ist diese Lösung noch experimentell, bis ich ihre Beweise vervollständige. Für diejenigen, die neugierig sind, sind die relevanten Art und Instanzen:

newtype Edge m r a b = Edge { unEdge :: a -> Pipe a b m r } 

instance (Monad m) => Category (Edge m r) where 
    id = Edge push 
    (Edge p2) . (Edge p1) = Edge (p1 >~> p2) 

instance (Monad m) => Arrow (Edge m r) where 
    arr f = Edge (push />/ respond . f) 
    first (Edge p) = Edge $ \(b, d) -> 
     evalStateP d $ (up \>\ unsafeHoist lift . p />/ dn) b 
     where 
     up() = do 
      (b, d) <- request() 
      lift $ put d 
      return b 
     dn c = do 
      d <- lift get 
      respond (c, d) 

instance (Monad m) => ArrowChoice (Edge m r) where 
    left (Edge k) = Edge (bef >=> (up \>\ (k />/ dn))) 
     where 
      bef x = case x of 
       Left b -> return b 
       Right d -> do 
        _ <- respond (Right d) 
        x2 <- request() 
        bef x2 
      up() = do 
       x <- request() 
       bef x 
      dn c = respond (Left c) 

Das Modell muss auch ein Monade-Transformator sein. Der Grund dafür ist, dass wir StateT in die Basis-Monade einbetten wollen, um den gemeinsamen Status zu verfolgen. In diesem Fall passt pipes die Rechnung.

Das letzte Teil des Puzzles ist ein ausgeklügeltes Beispiel aus der Praxis, ein komplexes Concurrent-System in ein reines Singlethread-Äquivalent zu zerlegen. Dazu benutze ich meine kommende rcpl Bibliothek (kurz für "read-simultant-print-loop"). Der Zweck der rcpl-Bibliothek besteht darin, der Konsole eine gleichzeitige Schnittstelle zur Verfügung zu stellen, mit der Sie Eingaben vom Benutzer lesen können, während gleichzeitig auf die Konsole gedruckt wird, ohne dass die gedruckte Ausgabe die Eingaben des Benutzers überlagert. Die Github-Repository für sie ist hier:

Link to Github Repository

Meine ursprüngliche Implementierung dieser Bibliothek hatte Pervasive Gleichzeitigkeit und Message-Passing, wurde aber von mehrere Gleichzeitigkeit Bugs geplagt, die ich nicht lösen konnte. Dann, als ich mvc (der Codename für mein FRP-ähnliches Framework, kurz für "Model-View-Controller") kam, dachte ich, dass rcpl ein ausgezeichneter Testfall wäre, um zu sehen, ob mvc für Prime-Time bereit war.

Ich nahm die gesamte Logik der rcpl und verwandelte es in eine einzige, reine Rohrleitung. Das finden Sie in this module, und die gesamte Logik ist vollständig in der rcplCore pipe enthalten.

Das ist nett, denn jetzt, wo die Implementierung rein ist, kann ich es überprüfen und bestimmte Eigenschaften überprüfen! Zum Beispiel ist eine Eigenschaft Ich möchte vielleicht zu Quick Check, dass pro Benutzer Tastendruck des x Taste genau ein Terminal-Befehl ist, die ich wie folgt angeben würde:

>>> quickCheck $ \n -> length ((`evalState` initialStatus) $ P.toListM $ each (replicate n (Key 'x')) >-> runEdge (rcplCore t)) == n || n < 0 

n ist die Anzahl der Male, die ich drücken der Schlüssel x. Das Ausführen dieses Tests erzeugt die folgende Ausgabe:

*** Failed! Falsifiable (after 17 tests and 6 shrinks): 
78 

QuickCheck entdeckte, dass meine Eigenschaft falsch war! Da der Code außerdem referenziell transparent ist, kann QuickCheck das Gegenbeispiel auf die minimale Reproduktionsverletzung eingrenzen. Nach 78 Tastendrücken gibt der Terminal-Treiber eine neue Zeile aus, da die Konsole 80 Zeichen breit ist und zwei Zeichen von der Eingabeaufforderung ("> " in diesem Fall) aufgenommen werden. Das ist die Art von Eigenschaft, die ich sehr schwierig hätte zu überprüfen, ob Nebenläufigkeit und IO mein gesamtes System infiziert.

Ein reines Setup ist aus einem anderen Grund großartig: alles ist komplett reproduzierbar! Wenn ich ein Protokoll aller eingehenden Ereignisse speichere, kann ich jederzeit, wenn ein Fehler auftritt, die Ereignisse erneut abspielen und den Testfall perfekt reproduzieren, den ich zu meiner Testsuite hinzufügen kann.

Der wichtigste Vorteil der Reinheit ist jedoch die Möglichkeit, sowohl informell als auch formal leichter über den Code nachzudenken. Wenn Sie den Haskell-Scheduler aus der Gleichung entfernen, können Sie Dinge statisch über Ihren Code nachweisen, die Sie nicht beweisen können, wenn Sie auf eine gleichzeitige Laufzeit mit einer informell angegebenen Semantik angewiesen sind. Dies erwies sich sogar für informelles Schließen als sehr nützlich, denn als ich meinen Code in mvc umwandelte, hatte er immer noch einige Bugs, aber diese waren viel einfacher zu debuggen und zu entfernen als die hartnäckigen Nebenläufigkeitsfehler aus meiner ersten Iteration.

Das rcpl Beispiel verwendet StateT globalen Zustand zwischen den verschiedenen Komponenten zu teilen, so dass die langatmige Antwort auf Ihre Frage ist: Sie StateT verwenden können, aber nur, wenn Sie Ihr System auf eine Single-Threaded-Version umwandeln. Zum Glück ist das möglich!

+1

Es tut mir leid, aber was ist die Verbindung zwischen Modell und dem Typ 'Edge'? Ist das gesamte MVC-Diagramm ein "Edge"? – chibro2

+1

Ja, 'Edge' sollte eigentlich' Model' heißen. Die Namen sind noch im Fluss. Auch, ja, das ganze MVC-Diagramm ist nur eine große 'Kante'. –

Verwandte Themen