Asynchrone Programmierung in funktionalen Sprachen

31

Ich bin hauptsächlich ein C / C ++ - Programmierer, was bedeutet, dass ich den größten Teil meiner Erfahrung mit prozeduralen und objektorientierten Paradigmen habe. Wie jedoch vielen C ++ - Programmierern bekannt ist, hat C ++ im Laufe der Jahre seinen Schwerpunkt auf einen funktionalen Stil verlagert, der schließlich in der Hinzufügung von Lambdas und Closures in C ++ 0x gipfelte.

Unabhängig davon habe ich zwar viel Erfahrung mit der Programmierung in einem funktionalen Stil mit C ++, aber nur sehr wenig Erfahrung mit tatsächlichen funktionalen Sprachen wie Lisp, Haskell usw.

Ich habe vor kurzem begonnen, diese Sprachen zu studieren, weil mich die Idee, dass "keine Nebenwirkungen" in rein funktionalen Sprachen auftreten, immer fasziniert hat, insbesondere in Bezug auf ihre Anwendungen für Parallelität und verteiltes Computing.

Ich bin jedoch verwirrt darüber, wie diese "keine Nebenwirkungen" -Philosophie bei asynchroner Programmierung funktioniert, da sie aus einem C ++ - Hintergrund stammt. Mit asynchroner Programmierung meine ich jeden Framework- / API- / Codierungsstil, der vom Benutzer bereitgestellte Ereignishandler zur Behandlung von Ereignissen absetzt, die asynchron auftreten (außerhalb des Programmflusses). Dies schließt asynchrone Bibliotheken wie Boost.ASIO oder auch nur einfaches altes C ein Signalhandler oder Java-GUI-Ereignishandler.

Allen gemeinsam ist, dass die Natur der asynchronen Programmierung die Erzeugung von Nebenwirkungen (Status) zu erfordern scheint, damit der Hauptablauf des Programms erkennt, dass ein asynchroner Ereignishandler aufgerufen wurde. In einem Framework wie Boost.ASIO ändert ein Ereignishandler normalerweise den Status eines Objekts, sodass die Wirkung des Ereignisses über die Lebensdauer der Ereignishandlerfunktion hinaus übertragen wird. Was kann ein Event-Handler sonst noch? Es kann keinen Wert an den Aufrufpunkt "zurückgeben", da kein Aufrufpunkt vorhanden ist. Die Ereignisbehandlungsroutine ist nicht Teil des Hauptablaufs des Programms. Sie kann sich also nur auf das eigentliche Programm auswirken, wenn Sie einen bestimmten Status (oder longjmpeinen anderen Ausführungspunkt) ändern .

Bei der asynchronen Programmierung geht es also anscheinend darum, Nebenwirkungen asynchron zu erzeugen. Dies scheint völlig im Widerspruch zu den Zielen der funktionalen Programmierung zu stehen. Wie werden diese beiden Paradigmen in funktionalen Sprachen (in der Praxis) in Einklang gebracht?

Charles Salvia
quelle
3
Wow, ich wollte gerade eine Frage wie diese schreiben und wusste nicht, wie ich sie stellen sollte und sah dies dann in den Vorschlägen!
Amogh Talpallikar

Antworten:

11

Alle Ihre Logik ist solide, außer dass ich denke, dass Ihr Verständnis der funktionalen Programmierung ein bisschen zu extrem ist. In der realen Welt geht es beim funktionalen Programmieren ebenso wie beim objektorientierten oder imperativen Programmieren um die Denkweise und wie Sie das Problem angehen. Sie können weiterhin Programme im Geiste der funktionalen Programmierung schreiben, während Sie den Anwendungsstatus ändern.

In der Tat müssen Sie den Anwendungsstatus ändern, um tatsächlich etwas zu tun . Die Haskell-Jungs werden Ihnen sagen, dass ihre Programme "pur" sind, weil sie alle ihre Zustandsänderungen in einer Monade zusammenfassen. Ihre Programme interagieren jedoch immer noch mit der Außenwelt. (Ansonsten, worum geht es!)

Funktionale Programmierung betont "keine Nebenwirkungen", wenn es Sinn macht. Um jedoch, wie Sie sagten, eine Programmierung in der realen Welt durchzuführen, müssen Sie den Zustand der Welt ändern. (Zum Beispiel auf Ereignisse reagieren, auf die Festplatte schreiben usw.)

Für weitere Informationen zur asynchronen Programmierung in funktionalen Sprachen empfehle ich dringend, dass Sie sich damit befassen Programmiermodell von F # für asynchrone Workflows . Es ermöglicht Ihnen, funktionierende Programme zu schreiben und dabei alle Details des Thread-Übergangs in einer Bibliothek zu verbergen. (In ähnlicher Weise wie bei Haskell-Monaden.)

Wenn der 'Körper' des Threads einfach einen Wert berechnet, liegt es immer noch im Funktionsparadigma, mehrere Threads zu erzeugen und Werte parallel berechnen zu lassen.

Chris Smith
quelle
5
Außerdem hilft es, sich Erlang anzuschauen. Die Sprache ist sehr einfach, rein (alle Daten sind unveränderlich) und dreht sich alles um asynchrone Verarbeitung.
9000
Wenn Sie die Vorteile des funktionalen Ansatzes kennen und den Status nur dann ändern, wenn es darauf ankommt, stellen Sie automatisch sicher, dass Sie wissen, wann Sie den Status ändern müssen und wie Sie die Kontrolle über solche Dinge behalten.
Amogh Talpallikar
Nicht einverstanden - eine Tatsache, dass ein Programm aus 'reinen' Funktionen besteht, bedeutet nicht, dass es nicht mit der Außenwelt zusammenwirkt, sondern dass jede Funktion in einem Programm für einen Satz von Argumenten immer dasselbe Ergebnis zurückgibt, und das ist (Reinheit) eine große Sache, weil aus praktischer Sicht - ein solches Programm wird weniger fehlerhaft, mehr "testbar", erfolgreiche Ausführung von Funktionen könnte mathematisch bewiesen werden.
Gill Bates
8

Das ist eine faszinierende Frage. Die interessanteste Sichtweise ist meiner Meinung nach der in Clojure eingeschlagene und in diesem Video erläuterte Ansatz:

http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey

Grundsätzlich wird folgende "Lösung" vorgeschlagen:

  • Sie schreiben den größten Teil Ihres Codes als klassische "reine" Funktionen mit unveränderlichen Datenstrukturen und ohne Nebenwirkungen
  • Nebenwirkungen werden durch die Verwendung von verwalteten Referenzen isoliert, die Änderungen gemäß den Regeln für den Transaktionsspeicher der Software steuern (dh alle Aktualisierungen in den veränderlichen Zustand erfolgen innerhalb einer ordnungsgemäßen isolierten Transaktion).
  • Wenn Sie diese Sicht der Welt betrachten, können Sie asynchrone "Ereignisse" als Auslöser für eine Transaktionsaktualisierung des veränderlichen Zustands sehen, bei der die Aktualisierung selbst eine reine Funktion ist.

Ich habe die Idee wahrscheinlich nicht so klar zum Ausdruck gebracht wie andere, aber ich hoffe, dies gibt die allgemeine Idee wieder - im Grunde wird ein gleichzeitiges STM-System verwendet, um die "Brücke" zwischen reiner funktionaler Programmierung und asynchroner Ereignisbehandlung zu schlagen.

mikera
quelle
6

Eine Anmerkung: Eine funktionale Sprache ist rein, ihre Laufzeit jedoch nicht.

Bei den Haskell-Laufzeiten handelt es sich beispielsweise um Warteschlangen, Thread-Multiplexing, Garbage-Collection usw., die alle nicht rein sind.

Ein gutes Beispiel ist Faulheit. Haskell unterstützt Lazy Evaluation (das ist eigentlich die Standardeinstellung). Sie erstellen einen Lazy-Wert, indem Sie eine Operation vorbereiten. Anschließend können Sie mehrere Kopien dieses Werts erstellen. Er ist weiterhin "faul", solange er nicht benötigt wird. Wenn das Ergebnis benötigt wird oder wenn die Laufzeit etwas Zeit findet, wird der Wert tatsächlich berechnet, und der Status des Lazy-Objekts ändert sich, um anzuzeigen, dass die Berechnung (erneut) nicht mehr erforderlich ist, um das Ergebnis zu erhalten. Es ist jetzt über alle Referenzen verfügbar, sodass sich der Status des Objekts geändert hat, obwohl es sich um eine reine Sprache handelt.

Matthieu M.
quelle
2

Ich bin verwirrt, wie diese "keine Nebenwirkungen" -Philosophie mit asynchroner Programmierung funktioniert. Mit asynchroner Programmierung meine ich ...

Das wäre dann der Punkt.

Ein Sound-No-Side-Effect-Stil ist nicht mit Frameworks kompatibel, die vom Status abhängen. Finde einen neuen Rahmen.

Mit dem WSGI-Standard von Python können wir beispielsweise keine Anwendungen für Nebenwirkungen erstellen.

Die Idee ist, dass die verschiedenen "Zustandsänderungen" sich in einer Umgebung von Werten widerspiegeln, die inkrementell aufgebaut werden können. Jede Anfrage ist eine Pipeline von Transformationen.

S.Lott
quelle
"Ermöglicht das Erstellen von Anwendungen ohne Nebenwirkungen" Ich denke, irgendwo fehlt ein Wort.
Christopher Mahan
1

Nachdem ich nach dem Erlernen von C die Kapselung von Borland C ++ gelernt hatte, als es in Borland C ++ an Vorlagen fehlte, die Generika ermöglichten, machte mich das Objektorientierungsparadigma unruhig. Etwas natürlichere Berechnungsmethoden schienen Daten durch Pipes zu filtern. Der nach außen gerichtete Datenstrom war vom nach innen gerichteten unveränderlichen Eingabestrom unabhängig und nicht als Nebeneffekt zu betrachten, dh, jede Datenquelle (oder jeder Filter) war unabhängig von anderen. Der Tastendruck (ein Beispielereignis) beschränkte die asynchronen Benutzereingabekombinationen auf die verfügbaren Schlüsselcodes. Funktionen verarbeiten Eingabeparameterargumente, und der von der Klasse eingeschlossene Status ist nur eine Abkürzung, um zu vermeiden, dass sich wiederholende Argumente explizit zwischen kleinen Teilmengen von Funktionen übergeben werden. Außerdem ist er vor dem gebundenen Kontext geschützt, um zu verhindern, dass diese Argumente von einer beliebigen Funktion aus missbraucht werden.

Das strikte Festhalten an einem bestimmten Paradigma verursacht die Unannehmlichkeit, mit undichten Abstraktionen umzugehen, z. Kommerzielle Laufzeiten wie JRE, DirectX, .net zielen in erster Linie auf objektorientierte Befürworter ab. Um die Unannehmlichkeiten einzuschränken, entscheiden sich Sprachen entweder für wissenschaftlich anspruchsvolle Monaden wie Haskell oder für flexible Multi-Paradigma-Unterstützung wie F #. Sofern die Kapselung nicht aus einem Anwendungsfall der Mehrfachvererbung nützlich ist, ist der Ansatz mit mehreren Paradigmen möglicherweise eine bessere Alternative zu einigen, manchmal komplexen, paradigmenspezifischen Programmiermustern.

Chawathe Vipul
quelle