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!
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. –
@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 ... –