Verwenden Sie Warteschlangen, um Funktionen zu entkoppeln / direkte Anrufe zu vermeiden?

8

Eine Art funktionale Programmier-Neuling-Frage hier:

Ich habe die Transkripte einiger von Rich Hickeys Vorträgen gelesen, und in einigen seiner bekannteren empfiehlt er, Warteschlangen als Alternative zum gegenseitigen Aufruf von Funktionen zu verwenden. (ZB in Design, Komposition und Leistung und in Simple Made Easy .)

Ich verstehe das in vielerlei Hinsicht nicht ganz:

  1. Spricht er davon, Daten in eine Warteschlange zu stellen und sie dann von jeder Funktion verwenden zu lassen? Anstelle von Funktion A, die Funktion B aufruft, um ihre eigene Berechnung durchzuführen, muss Funktion B ihre Ausgabe in eine Warteschlange stellen und dann Funktion A darauf zugreifen lassen. Oder sprechen wir alternativ davon, Funktionen in eine Warteschlange zu stellen und sie dann sukzessive auf Daten anzuwenden (sicherlich nicht, da dies eine massive Mutation beinhalten würde, oder? Und auch die Multiplikation von Warteschlangen für Funktionen mit mehreren Aritäten oder wie Bäume oder ähnliches? )

  2. Wie macht das die Sache einfacher? Meine Intuition wäre, dass diese Strategie mehr Komplexität schaffen würde, weil die Warteschlange eine Art Zustand wäre, und dann müssen Sie sich Sorgen machen, "was ist, wenn sich eine andere Funktion einschleicht und einige Daten in die Warteschlange stellt?"

Eine Antwort auf eine Implementierungsfrage zu SO legt nahe, dass die Idee eine Reihe verschiedener Warteschlangen erstellt. Jede Funktion stellt ihre Ausgabe in eine eigene Warteschlange (??). Das verwirrt mich aber auch, denn wenn Sie eine Funktion einmal ausführen, warum braucht sie dann eine Warteschlange für ihre Ausgabe, wenn Sie diese Ausgabe einfach nehmen und einen Namen als (var, atom, entry in a big) darauf setzen könnten Hash-Tabelle, was auch immer). Wenn eine Funktion dagegen mehrmals ausgeführt wird und Sie ihre Ausgabe in eine Warteschlange stellen, haben Sie sich selbst wieder den Status zugefügt, und Sie müssen sich Gedanken über die Reihenfolge machen, in der alles aufgerufen wird. Downstream-Funktionen werden weniger rein. usw.

Offensichtlich verstehe ich den Punkt hier nicht. Kann jemand etwas erklären?

Paul Gowder
quelle
Ich habe in Ihrem ersten Link keinen einzigen Verweis auf eine Warteschlange gesehen, obwohl ich Ihnen gewähren werde, dass der Beitrag unglaublich lang ist und ich ihn möglicherweise verpasst habe. Es scheint eher ein Gespräch über Kunst als über Programmierung zu sein.
Robert Harvey
Warteschlangen werden im zweiten Artikel zweimal kurz erwähnt, aber nicht erläutert. In jedem Fall gibt es schon seit einiger Zeit die Idee, eine Messaging-Warteschlange für die Kommunikation zwischen Anwendungen oder Modulen zu verwenden. Es ist unwahrscheinlich, dass Sie dies in einer einzelnen Anwendung tun, es sei denn, Sie haben eine Verarbeitungspipeline oder eine State Engine erstellt.
Robert Harvey
Es ist in dem Absatz unter der Folie mit der Überschrift "Zeit / Reihenfolge / Fluss auseinander nehmen" ("Sie können Systeme auseinander brechen, so dass es weniger direkte Anrufe gibt. Sie können Warteschlangen verwenden, um dies zu tun.")
Paul Gowder
3
Es lohnt sich nicht, darauf eine vollständige Antwort zu geben, aber er gibt wirklich nur eine unscharfe Beschreibung für das Konzept von Jobpools und ereignisgesteuerter Programmierung. Sie packen also einen Funktionsaufruf in ein generisches JobObjekt, verschieben ihn in eine Warteschlange und lassen einen oder mehrere Arbeitsthreads an dieser Warteschlange arbeiten. Der Jobschickt dann Jobnach Abschluss weitere s in die Warteschlange. Rückgabewerte werden in diesem Konzept durch Rückrufe ersetzt. Es ist ein Albtraum, Fehler zu beheben und zu überprüfen, da Ihnen ein Aufrufstapel fehlt, und aus demselben Grund effizient und flexibel.
Ext3h
Vielen Dank. Vielleicht ist mein eigentliches Problem, dass ich Messaging nicht verstehe! (Heh, Zeit, Smalltalk zu lernen? :-))
Paul Gowder

Antworten:

6

Es ist eher eine Entwurfsübung als eine allgemeine Empfehlung. Normalerweise stellen Sie keine Warteschlange zwischen all Ihren direkten Funktionsaufrufen. Das wäre lächerlich. Wenn Sie Ihre Funktionen jedoch nicht so gestalten, als ob eine Warteschlange zwischen den direkten Funktionsaufrufen eingefügt werden könnte, können Sie nicht zu Recht behaupten, wiederverwendbaren und zusammensetzbaren Code geschrieben zu haben. Das ist der Punkt, den Rich Hickey macht.

Dies ist beispielsweise ein Hauptgrund für den Erfolg von Apache Spark . Sie schreiben Code, der aussieht, als würde er direkte Funktionsaufrufe für lokale Sammlungen ausführen, und das Framework übersetzt diesen Code in die Weitergabe von Nachrichten an Warteschlangen zwischen Clusterknoten. Die Art von einfachem, zusammensetzbarem und wiederverwendbarem Codierungsstil, den Rich Hickey befürwortet, macht dies möglich.

Karl Bielefeldt
quelle
Aber ist das nicht nur eine Änderung im Methodenbindungsprozess? Am Ende des Tages ist ein Funktionsaufruf nur ein Funktionsaufruf, oder? Was danach passiert, hängt davon ab, was die Funktion tut. Es geht also weniger um Funktionsaufrufe als darum, wie die zugrunde liegende Infrastruktur aufgebaut ist.
Robert Harvey
1
Anders ausgedrückt, welche Änderung würden Sie an Ihren Funktionsaufrufen vornehmen, um sie "warteschlangenfreundlich" zu machen?
Robert Harvey
5
Denken Sie daran, dass der Code am anderen Ende der Warteschlange nicht unbedingt auf denselben Speicher und dieselbe E / A zugreifen muss. Warteschlangenfreundliche Funktionen sind frei von Nebenwirkungen und erwarten Eingaben und erzeugen Ausgaben, die unveränderlich und leicht serialisierbar sind. Das ist kein so einfacher Test für viele Codebasen.
Karl Bielefeldt
3
Ah, also "funktionale Programmierfreundlichkeit" dann. Ein bisschen macht Sinn, da es Rich Hickey ist, der darüber spricht.
Robert Harvey
0

Zu beachten ist, dass Sie mit der funktionalen Programmierung Funktionen indirekt über Mediatorobjekte miteinander verbinden können , die sich darum kümmern, Argumente für die Funktionen zu beschaffen und ihre Ergebnisse flexibel an Empfänger weiterzuleiten, die ihre Ergebnisse wünschen. Angenommen, Sie haben einen einfachen Direktaufrufcode, der wie in diesem Beispiel in Haskell aussieht:

myThing :: A -> B -> C
myThing a b = f a (g a b)

Nun, mit Haskells ApplicativeKlasse und ihren <$>und <*>Operatoren können wir diesen Code mechanisch umschreiben:

myThing :: Applicative f => f A -> f B -> f C
myThing a b = f <$> a <*> (g <$> a <*> b)

... wo jetzt myThingnicht mehr direkt angerufen wird fund gsondern durch einige Vermittler des Typs verbunden wird f. So fkönnte beispielsweise ein StreamTyp sein, der von einer Bibliothek bereitgestellt wird, die eine Schnittstelle zu einem Warteschlangensystem bereitstellt. In diesem Fall hätten wir diesen Typ:

myThing :: Stream A -> Stream B -> Stream C
myThing a b = f <$> a <*> (g <$> a <*> b)

Systeme wie dieses existieren. Tatsächlich können Sie Java 8-Streams als eine Version dieses Paradigmas betrachten. Sie erhalten folgenden Code:

List<Integer> transactionsIds = 
    transactions.parallelStream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

Hier verwenden Sie folgende Funktionen:

  • t -> t.getType() == Transaction.GROCERY
  • comparing(Transaction::getValue).reversed()
  • Transaction::getId
  • toList()

... und anstatt dass sie sich direkt anrufen, verwenden Sie das StreamSystem, um zwischen ihnen zu vermitteln. In diesem Codebeispiel wird die Transaction::getIdFunktion nicht direkt Streamaufgerufen, sondern mit den Transaktionen, die zuvor überlebt haben filter. Sie können sich die StreamWarteschlange als eine sehr minimale Art von Warteschlange vorstellen, die Funktionen indirekt koppelt und Werte zwischen ihnen weiterleitet.

Sacundim
quelle