Was ist (funktionale) reaktive Programmierung?


1148

Ich habe den Wikipedia-Artikel über reaktive Programmierung gelesen . Ich habe auch den kleinen Artikel über funktionale reaktive Programmierung gelesen . Die Beschreibungen sind ziemlich abstrakt.

  1. Was bedeutet funktionale reaktive Programmierung (FRP) in der Praxis?
  2. Woraus besteht reaktive Programmierung (im Gegensatz zu nicht reaktiver Programmierung?)?

Mein Hintergrund ist in imperativen / OO-Sprachen, daher wäre eine Erklärung, die sich auf dieses Paradigma bezieht, willkommen.


159
Hier ist ein Typ mit einer aktiven Fantasie und guten Fähigkeiten zum Geschichtenerzählen, der das Ganze übernimmt. paulstovell.com/reactive-programming
melaos

39
Jemand muss wirklich eine "Functional Reactive Programming For Dummies" für alle uns Autodidakten hier draußen schreiben. Jede Ressource, die ich gefunden habe, selbst Elm, scheint davon auszugehen, dass Sie in den letzten fünf Jahren einen Master in CS erhalten haben. Diejenigen, die sich mit FRP auskennen, scheinen die Fähigkeit, die Angelegenheit aus naiver Sicht zu betrachten, völlig verloren zu haben, was für das Lehren, Trainieren und Evangelisieren von entscheidender Bedeutung ist.
TechZen

26
Ein weiteres exzellentes FRP-Intro: Die Einführung in die reaktive Programmierung, die mein Kollege André
Jonik

5
Eines der besten, das ich je gesehen habe. Beispielbasiert: gist.github.com/staltz/868e7e9bc2a7b8c1f754
Razmig

2
Ich finde die Tabellenkalkulationsanalogie als ersten groben Eindruck sehr hilfreich (siehe Bobs Antwort: stackoverflow.com/a/1033066/1593924 ). Eine Tabellenkalkulationszelle reagiert auf Änderungen in anderen Zellen (zieht), greift jedoch nicht nach anderen und ändert andere (drückt nicht). Das Endergebnis ist, dass Sie eine Zelle ändern können und eine Unmenge anderer "unabhängig" ihre eigenen Anzeigen aktualisieren.
Jon Coombs

Antworten:


931

Wenn Sie ein Gefühl für FRP bekommen möchten, können Sie mit dem alten Fran-Tutorial von 1998 beginnen, das animierte Illustrationen enthält. Beginnen Sie für Artikel mit Functional Reactive Animation und folgen Sie den Links auf dem Publikationslink auf meiner Homepage und dem FRP- Link im Haskell-Wiki .

Persönlich denke ich gerne darüber nach, was FRP bedeutet, bevor ich mich mit der möglichen Implementierung befasse. (Code ohne Spezifikation ist eine Antwort ohne Frage und daher "nicht einmal falsch".) Daher beschreibe ich FRP nicht in Repräsentations- / Implementierungsbegriffen wie Thomas K in einer anderen Antwort (Grafiken, Knoten, Kanten, Brennen, Ausführung, usw). Es gibt viele mögliche Implementierungsstile, aber keine Implementierung sagt aus, was FRP ist .

Ich stimme mit Laurence Gs einfacher Beschreibung überein, dass es bei FRP um "Datentypen geht, die einen Wert 'über die Zeit' darstellen". Die konventionelle imperative Programmierung erfasst diese dynamischen Werte nur indirekt durch Zustand und Mutationen. Die gesamte Geschichte (Vergangenheit, Gegenwart, Zukunft) hat keine erstklassige Darstellung. Darüber hinaus können nur sich diskret entwickelnde Werte (indirekt) erfasst werden, da das imperative Paradigma zeitlich diskret ist. Im Gegensatz dazu erfasst FRP diese sich entwickelnden Werte direkt und hat keine Schwierigkeiten mit sich kontinuierlich entwickelnden Werten.

FRP ist auch insofern ungewöhnlich, als es gleichzeitig stattfindet, ohne das theoretische und pragmatische Rattennest zu verletzen, das die zwingende Parallelität plagt. Semantisch gesehen ist die Parallelität von FRP feinkörnig , bestimmt und kontinuierlich . (Ich spreche von Bedeutung, nicht von Implementierung. Eine Implementierung kann Parallelität oder Parallelität beinhalten oder nicht.) Semantische Determiniertheit ist sehr wichtig für das Denken, sowohl rigoros als auch informell. Parallelität erhöht die Komplexität der imperativen Programmierung enorm (aufgrund nichtdeterministischer Verschachtelung), ist jedoch in FRP mühelos.

Was ist FRP? Du hättest es selbst erfinden können. Beginnen Sie mit diesen Ideen:

  • Dynamische / sich entwickelnde Werte (dh Werte "über die Zeit") sind an sich erstklassige Werte. Sie können sie definieren und kombinieren, sie an und aus Funktionen übergeben. Ich habe diese Dinge "Verhalten" genannt.

  • Verhaltensweisen werden aus wenigen Grundelementen wie konstantem (statischem) Verhalten und Zeit (wie einer Uhr) und dann mit sequentieller und paralleler Kombination aufgebaut. n Verhaltensweisen werden kombiniert, indem eine n-fache Funktion (auf statische Werte) "punktweise" angewendet wird, dh kontinuierlich über die Zeit.

  • Um diskrete Phänomene zu berücksichtigen, müssen Sie eine andere Art (Familie) von "Ereignissen" haben, von denen jedes einen Strom (endlich oder unendlich) von Ereignissen aufweist. Jedem Vorkommen ist eine Zeit und ein Wert zugeordnet.

  • Spielen Sie einige Beispiele, um das kompositorische Vokabular zu erstellen, aus dem alle Verhaltensweisen und Ereignisse aufgebaut werden können. Dekonstruieren Sie weiter in Teile, die allgemeiner / einfacher sind.

  • Damit Sie wissen, dass Sie auf festem Grund stehen, geben Sie dem gesamten Modell eine kompositorische Grundlage unter Verwendung der Technik der Denotationssemantik. Dies bedeutet lediglich, dass (a) jeder Typ einen entsprechenden einfachen und präzisen mathematischen Typ von "Bedeutungen" hat und ( b) Jedes Grundelement und jeder Operator hat eine einfache und genaue Bedeutung in Abhängigkeit von der Bedeutung der Bestandteile. Mischen Sie niemals Implementierungsüberlegungen in Ihren Explorationsprozess. Wenn diese Beschreibung für Sie Kauderwelsch ist, konsultieren Sie (a) Denotationsdesign mit Typklassenmorphismen , (b) Push-Pull-funktionale reaktive Programmierung (Ignorieren der Implementierungsbits) und (c) die Wikibooks-Seite Denotational Semantics Haskell. Beachten Sie, dass die Denotationssemantik aus zwei Teilen besteht, von den beiden Gründern Christopher Strachey und Dana Scott: dem einfacheren und nützlicheren Strachey-Teil und dem schwierigeren und weniger nützlichen (für das Software-Design) Scott-Teil.

Wenn Sie sich an diese Grundsätze halten, werden Sie wahrscheinlich mehr oder weniger etwas im Sinne von FRP erhalten.

Woher habe ich diese Prinzipien? Beim Software-Design stelle ich immer die gleiche Frage: "Was bedeutet das?". Die denotationale Semantik gab mir einen genauen Rahmen für diese Frage, der zu meiner Ästhetik passt (im Gegensatz zur operativen oder axiomatischen Semantik, die mich beide unbefriedigt lässt). Also habe ich mich gefragt, was Verhalten ist. Ich erkannte bald, dass die zeitlich diskrete Natur der imperativen Berechnung eher eine Anpassung an einen bestimmten Maschinenstil als eine natürliche Beschreibung des Verhaltens selbst ist. Die einfachste genaue Beschreibung des Verhaltens, die ich mir vorstellen kann, ist einfach "Funktion der (kontinuierlichen) Zeit", das ist also mein Modell. Dieses Modell behandelt auf wunderbare Weise die kontinuierliche, deterministische Parallelität mit Leichtigkeit und Anmut.

Es war eine ziemliche Herausforderung, dieses Modell korrekt und effizient zu implementieren, aber das ist eine andere Geschichte.


78
Ich war mir der funktionalen reaktiven Programmierung bewusst. Es scheint mit meiner eigenen Forschung (in interaktiven statistischen Grafiken) zu tun zu haben, und ich bin sicher, dass viele der Ideen für meine Arbeit hilfreich wären. Ich finde es jedoch sehr schwierig, an der Sprache vorbei zu kommen - muss ich wirklich etwas über "Denotationssemantik" und "Typklassenmorphismen" lernen, um zu verstehen, was los ist? Eine allgemeine Einführung des Publikums in das Thema wäre sehr nützlich.
Hadley

212
@Conal: Sie wissen genau, wovon Sie sprechen, aber Ihre Sprache setzt voraus, dass ich in Computermathematik promoviert habe, was ich nicht tue. Ich habe einen Hintergrund in Systemtechnik und mehr als 20 Jahre Erfahrung mit Computern und Programmiersprachen. Trotzdem habe ich das Gefühl, dass Ihre Antwort mich verblüfft. Ich fordere Sie auf, Ihre Antwort auf Englisch erneut zu veröffentlichen ;-)
mindplay.dk

50
@ minplay.dk: Ihre Bemerkungen geben mir nicht viel zu erzählen, was Sie insbesondere nicht verstehen, und ich bin nicht geneigt, wilde Vermutungen darüber anzustellen, nach welcher bestimmten Untergruppe von Englisch Sie suchen. Ich lade Sie jedoch ein, genau zu sagen, auf welche Aspekte meiner obigen Erklärung Sie stoßen, damit ich und andere Ihnen helfen können. Gibt es beispielsweise bestimmte Wörter, die Sie definieren möchten, oder Konzepte, für die Referenzen hinzugefügt werden sollen? Ich mag es wirklich, die Klarheit und Zugänglichkeit meines Schreibens zu verbessern - ohne es zu beschwichtigen.
Conal

27
"Bestimmtheit" / "Bestimmen" bedeutet, dass es einen einzelnen, genau definierten korrekten Wert gibt. Im Gegensatz dazu können fast alle Formen der imperativen Parallelität je nach Planer oder ob Sie suchen oder nicht, unterschiedliche Antworten geben, und sie können sogar zum Stillstand kommen. "Semantisch" (und insbesondere "Bezeichnung") bezieht sich auf den Wert ("Bezeichnung") eines Ausdrucks oder einer Darstellung im Gegensatz zu "operativ" (wie die Antwort berechnet wird oder wie viel Raum und / oder Zeit von was verbraucht wird Art der Maschine).
Conal

18
Ich stimme @ mindplay.dk zu, obwohl ich nicht damit prahlen kann, sehr lange auf dem Feld zu sein. Obwohl es so aussah, als wüssten Sie, wovon Sie sprechen, gab es mir kein schnelles, kurzes und einfaches Verständnis dafür, was dies ist, da ich verwöhnt genug bin, um mit SO zu rechnen. Diese Antwort brachte mich in erster Linie zu einer Menge neuer Fragen, ohne meine erste wirklich zu beantworten. Ich hoffe, dass das Teilen der Erfahrung, auf diesem Gebiet noch relativ unwissend zu sein, Ihnen einen Einblick geben kann, wie einfach und kurz Sie wirklich sein müssen. Ich komme übrigens aus einem ähnlichen Umfeld wie das OP.
Aske B.

739

In der reinen Funktionsprogrammierung treten keine Nebenwirkungen auf. Bei vielen Arten von Software (z. B. bei allen Benutzerinteraktionen) sind auf einer bestimmten Ebene Nebenwirkungen erforderlich.

Eine Möglichkeit, nebenwirkungsähnliches Verhalten zu erzielen und gleichzeitig einen funktionalen Stil beizubehalten, ist die Verwendung einer funktionalen reaktiven Programmierung. Dies ist die Kombination aus funktionaler Programmierung und reaktiver Programmierung. (Der Wikipedia-Artikel, auf den Sie verlinkt haben, handelt von letzterem.)

Die Grundidee hinter reaktiver Programmierung ist, dass es bestimmte Datentypen gibt, die einen Wert "über die Zeit" darstellen. Berechnungen, die diese sich im Laufe der Zeit ändernden Werte beinhalten, haben selbst Werte, die sich im Laufe der Zeit ändern.

Beispielsweise könnten Sie die Mauskoordinaten als ein Paar von Ganzzahl-über-Zeit-Werten darstellen. Nehmen wir an, wir hatten so etwas wie (das ist Pseudocode):

x = <mouse-x>;
y = <mouse-y>;

Zu jedem Zeitpunkt hätten x und y die Koordinaten der Maus. Im Gegensatz zur nicht reaktiven Programmierung müssen wir diese Zuordnung nur einmal vornehmen, und die x- und y-Variablen bleiben automatisch "auf dem neuesten Stand". Aus diesem Grund arbeiten reaktive Programmierung und funktionale Programmierung so gut zusammen: Durch reaktive Programmierung müssen keine Variablen mehr mutiert werden, und Sie können dennoch viel tun, was Sie mit variablen Mutationen erreichen können.

Wenn wir dann einige darauf basierende Berechnungen durchführen, sind die resultierenden Werte auch Werte, die sich im Laufe der Zeit ändern. Zum Beispiel:

minX = x - 16;
minY = y - 16;
maxX = x + 16;
maxY = y + 16;

In diesem Beispiel ist minXimmer 16 kleiner als die x-Koordinate des Mauszeigers. Mit reaktivbewussten Bibliotheken könnte man dann etwas sagen wie:

rectangle(minX, minY, maxX, maxY)

Ein 32x32-Feld wird um den Mauszeiger herum gezeichnet und verfolgt, wo immer er sich bewegt.

Hier ist ein ziemlich gutes Papier über funktionale reaktive Programmierung .


25
Reaktive Programmierung ist also eine Form der deklarativen Programmierung?
Troelskn

31
> Reaktive Programmierung ist also eine Form der deklarativen Programmierung? Funktionale reaktive Programmierung ist eine Form der funktionalen Programmierung, die eine Form der deklarativen Programmierung ist.
Conal

7
@ user712092 Nicht wirklich, nein. Wenn ich zum Beispiel sqrt(x)C mit Ihrem Makro aufrufe, berechnet das nur sqrt(mouse_x())und gibt mir ein Double zurück. In einem echten funktionellen reaktiven System sqrt(x)würde ein neues "Double Over Time" zurückgegeben. Wenn Sie versuchen würden, ein FR-System mit zu simulieren #define, müssten Sie Variablen zugunsten von Makros abschwören. FR-Systeme berechnen normalerweise auch nur dann Daten neu, wenn sie neu berechnet werden müssen. Die Verwendung von Makros würde bedeuten, dass Sie ständig alles neu bewerten, bis hin zu den Unterausdrücken.
Laurence Gonsalves

4
"Für viele Arten von Software (zum Beispiel alles, was mit Benutzerinteraktion zu tun hat) sind auf einer bestimmten Ebene Nebenwirkungen erforderlich." Und vielleicht nur auf Implementierungsebene. Die Implementierung einer reinen, faulen funktionalen Programmierung hat viele Nebenwirkungen, und einer der Erfolge des Paradigmas besteht darin, viele dieser Effekte aus dem Programmiermodell herauszuhalten. Meine eigenen Versuche mit funktionalen Benutzeroberflächen legen nahe, dass sie auch vollständig ohne Nebenwirkungen programmiert werden können.
Conal

4
@tieTYT x wird niemals neu zugewiesen / mutiert. Der Wert von x ist die zeitliche Abfolge von Werten. Eine andere Sichtweise ist, dass anstelle von x mit einem "normalen" Wert wie einer Zahl der Wert von x (konzeptionell) eine Funktion ist, die Zeit als Parameter benötigt. (Dies ist ein bisschen zu einfach. Sie können keine Zeitwerte erstellen, mit denen Sie die Zukunft von Dingen wie der
Laurence Gonsalves

144

Eine einfache Möglichkeit, eine erste Vorstellung davon zu bekommen, wie es ist, besteht darin, sich vorzustellen, dass Ihr Programm eine Tabelle ist und alle Ihre Variablen Zellen sind. Wenn sich eine der Zellen in einer Tabelle ändert, ändern sich auch alle Zellen, die auf diese Zelle verweisen. Bei FRP ist es genauso. Stellen Sie sich nun vor, dass sich einige der Zellen von selbst ändern (oder eher von außen stammen): In einer GUI-Situation wäre die Position der Maus ein gutes Beispiel.

Das fehlt zwangsläufig ziemlich viel. Die Metapher bricht ziemlich schnell zusammen, wenn Sie tatsächlich ein FRP-System verwenden. Zum einen gibt es normalerweise auch Versuche, diskrete Ereignisse zu modellieren (z. B. wenn die Maus angeklickt wird). Ich schreibe das hier nur, um Ihnen eine Vorstellung davon zu geben, wie es ist.


3
Ein äußerst passendes Beispiel. Es ist großartig, das theoretische Zeug zu haben, und vielleicht bekommen einige Leute die Implikationen davon, ohne auf ein grundlegendes Beispiel zurückzugreifen, aber ich muss damit beginnen, was es für mich tut, nicht was es abstrakt ist. Was ich erst kürzlich bekommen habe (aus den Rx-Gesprächen von Netflix!), Ist, dass RP (oder jedenfalls Rx) diese "sich ändernden Werte" erstklassig macht und Sie darüber nachdenken oder Funktionen schreiben können, die Dinge mit ihnen tun. Schreiben Sie Funktionen, um Tabellenkalkulationen oder Zellen zu erstellen, wenn Sie möchten. Und es behandelt, wenn ein Wert endet (verschwindet) und Sie können automatisch bereinigen.
Benjohn

In diesem Beispiel wird der Unterschied zwischen ereignisgesteuerter Programmierung und reaktivem Ansatz hervorgehoben, bei dem Sie lediglich die Abhängigkeiten für die Verwendung von intelligentem Routing deklarieren.
Kinjelom

131

Für mich geht es um 2 verschiedene Bedeutungen des Symbols =:

  1. In Mathe x = sin(t)bedeutet das xein anderer Name für sin(t). Schreiben x + yist also dasselbe wie sin(t) + y. Funktionale reaktive Programmierung ist in dieser Hinsicht wie Mathematik: Wenn Sie schreiben x + y, wird sie mit dem Wert berechnet, tder zum Zeitpunkt der Verwendung vorliegt.
  2. In C-ähnlichen Programmiersprachen (imperativen Sprachen) x = sin(t)ist eine Zuweisung: Dies bedeutet, dass xder Wert sin(t) gespeichert wird, der zum Zeitpunkt der Zuweisung genommen wurde.

5
Gute Erklärung. Ich denke, Sie könnten auch hinzufügen, dass "Zeit" im Sinne von FRP normalerweise "jede Änderung von externen Eingaben" ist. Jedes Mal, wenn eine externe Kraft eine Eingabe von FRP ändert, haben Sie die "Zeit" vorwärts verschoben und alles, was von der Änderung betroffen ist, erneut neu berechnet.
Didier A.

4
In mathematischen x = sin(t)Mitteln xist der Wert sin(t)für das gegebene t. Es ist kein anderer Name für sin(t)als Funktion. Sonst wäre es x(t) = sin(t).
Dmitri Zaitsev

+ Dmitri Zaitsev Gleichheitszeichen hat in der Mathematik mehrere Bedeutungen. Eine davon ist, dass Sie sie immer dann, wenn Sie die linke Seite sehen , gegen die rechte Seite austauschen können . Zum Beispiel 2 + 3 = 5oder a**2 + b**2 = c**2.
user712092

71

OK, aus dem Hintergrundwissen und dem Lesen der Wikipedia-Seite, auf die Sie hingewiesen haben, scheint es, dass reaktive Programmierung so etwas wie Datenfluss-Computing ist, aber mit spezifischen externen "Stimuli", die eine Reihe von Knoten auslösen, um ihre Berechnungen auszulösen und durchzuführen.

Dies eignet sich beispielsweise sehr gut für das UI-Design, bei dem durch Berühren eines Steuerelements für die Benutzeroberfläche (z. B. des Lautstärkereglers einer Musikwiedergabeanwendung) möglicherweise verschiedene Anzeigeelemente und die tatsächliche Lautstärke der Audioausgabe aktualisiert werden müssen. Wenn Sie das Volumen ändern (z. B. einen Schieberegler), entspricht dies dem Ändern des Werts, der einem Knoten in einem gerichteten Diagramm zugeordnet ist.

Verschiedene Knoten mit Kanten von diesem "Volumenwert" -Knoten würden automatisch ausgelöst, und alle erforderlichen Berechnungen und Aktualisierungen würden natürlich die Anwendung durchlaufen. Die Anwendung "reagiert" auf den Benutzerreiz. Funktionale reaktive Programmierung wäre nur die Umsetzung dieser Idee in einer funktionalen Sprache oder allgemein innerhalb eines funktionalen Programmierparadigmas.

Wenn Sie mehr über "Dataflow Computing" erfahren möchten, suchen Sie in Wikipedia oder mit Ihrer bevorzugten Suchmaschine nach diesen beiden Wörtern. Die allgemeine Idee lautet: Das Programm ist ein gerichteter Graph von Knoten, die jeweils eine einfache Berechnung durchführen. Diese Knoten sind durch Diagrammverknüpfungen miteinander verbunden, die die Ausgaben einiger Knoten mit den Eingaben anderer Knoten versehen.

Wenn ein Knoten ausgelöst wird oder seine Berechnung durchführt, werden die entsprechenden Eingänge der an seine Ausgänge angeschlossenen Knoten "ausgelöst" oder "markiert". Jeder Knoten, bei dem alle Eingänge ausgelöst / markiert / verfügbar sind, wird automatisch ausgelöst. Das Diagramm kann implizit oder explizit sein, je nachdem, wie die reaktive Programmierung genau implementiert ist.

Knoten können als parallel ausgelöst betrachtet werden, werden jedoch häufig seriell oder mit begrenzter Parallelität ausgeführt (z. B. werden möglicherweise einige Threads ausgeführt). Ein berühmtes Beispiel war die Manchester Dataflow Machine , die (IIRC) eine getaggte Datenarchitektur verwendete, um die Ausführung von Knoten im Diagramm über eine oder mehrere Ausführungseinheiten zu planen. Das Datenfluss-Computing eignet sich ziemlich gut für Situationen, in denen das asynchrone Auslösen von Berechnungen, die zu Kaskaden von Berechnungen führen, besser funktioniert als der Versuch, die Ausführung von einer Uhr (oder von Uhren) steuern zu lassen.

Reaktive Programmierung importiert diese Idee der "Ausführungskaskade" und scheint das Programm datenflussartig zu denken, jedoch mit der Maßgabe, dass einige der Knoten mit der "Außenwelt" verbunden sind und die Ausführungskaskaden ausgelöst werden, wenn diese sensorischen -ähnliche Knoten ändern sich. Die Programmausführung würde dann wie etwas aussehen, das einem komplexen Reflexbogen entspricht. Das Programm kann zwischen Stimuli grundsätzlich sitzend sein oder nicht oder kann sich zwischen Reizen in einem grundsätzlich sitzenden Zustand niederlassen.

"nicht reaktive" Programmierung wäre eine Programmierung mit einer ganz anderen Sicht auf den Ausführungsfluss und die Beziehung zu externen Eingaben. Es ist wahrscheinlich etwas subjektiv, da die Leute wahrscheinlich versucht sind, etwas zu sagen, das auf externe Eingaben reagiert und auf sie "reagiert". Wenn man jedoch den Geist der Sache betrachtet, ist ein Programm, das eine Ereigniswarteschlange in einem festgelegten Intervall abfragt und alle gefundenen Ereignisse an Funktionen (oder Threads) sendet, weniger reaktiv (da es nur in einem festgelegten Intervall auf Benutzereingaben achtet). Auch hier ist es der Geist der Sache: Man kann sich vorstellen, eine Polling-Implementierung mit einem schnellen Polling-Intervall in ein System auf einer sehr niedrigen Ebene zu integrieren und darüber auf reaktive Weise zu programmieren.


1
OK, oben gibt es einige gute Antworten. Soll ich meinen Beitrag entfernen? Wenn ich zwei oder drei Leute sehe, die sagen, dass es nichts hinzufügt, werde ich es löschen, es sei denn, die Anzahl der hilfreichen Personen steigt. Es macht keinen Sinn, es hier zu lassen, es sei denn, es bringt etwas von Wert.
Thomas Kammeyer

3
Sie haben den Datenfluss erwähnt, damit IMHO ein gewisser Mehrwert entsteht.
Rainer Joswig

So soll QML sein, wie es scheint;)
mlvljr

3
Für mich war diese Antwort am einfachsten zu verstehen, insbesondere weil natürliche Analoga wie "Welligkeit durch die Anwendung" und "sensorische Knoten" verwendet wurden. Großartig!
Akseli Palén

1
Leider ist der Link zur Manchester Dataflow Machine nicht mehr vorhanden.
Pac0

65

Nachdem ich viele Seiten über FRP gelesen hatte, stieß ich endlich auf dieses aufschlussreiche Schreiben über FRP, das mir endlich verständlich machte, worum es bei FRP wirklich geht.

Ich zitiere unten Heinrich Apfelmus (Autor der reaktiven Banane).

Was ist die Essenz der funktionalen reaktiven Programmierung?

Eine häufige Antwort wäre, dass es bei „FRP darum geht, ein System anhand zeitlich variierender Funktionen anstelle eines veränderlichen Zustands zu beschreiben“, und das wäre sicherlich nicht falsch. Dies ist der semantische Standpunkt. Aber meiner Meinung nach ergibt sich die tiefere und zufriedenstellendere Antwort aus dem folgenden rein syntaktischen Kriterium:

Die Essenz der funktionalen reaktiven Programmierung besteht darin, das dynamische Verhalten eines Werts zum Zeitpunkt der Deklaration vollständig anzugeben.

Nehmen Sie zum Beispiel einen Zähler: Sie haben zwei Schaltflächen mit den Bezeichnungen „Auf“ und „Ab“, mit denen Sie den Zähler erhöhen oder verringern können. Sie müssen unbedingt zuerst einen Anfangswert angeben und ihn dann ändern, wenn eine Taste gedrückt wird. etwas wie das:

counter := 0                               -- initial value
on buttonUp   = (counter := counter + 1)   -- change it later
on buttonDown = (counter := counter - 1)

Der Punkt ist, dass zum Zeitpunkt der Deklaration nur der Anfangswert für den Zähler angegeben wird; Das dynamische Verhalten des Zählers ist im Rest des Programmtextes enthalten. Im Gegensatz dazu spezifiziert die funktionale reaktive Programmierung das gesamte dynamische Verhalten zum Zeitpunkt der Deklaration wie folgt:

counter :: Behavior Int
counter = accumulate ($) 0
            (fmap (+1) eventUp
             `union` fmap (subtract 1) eventDown)

Wann immer Sie die Dynamik des Zählers verstehen wollen, müssen Sie nur seine Definition betrachten. Alles, was damit passieren kann, wird auf der rechten Seite angezeigt. Dies steht in starkem Gegensatz zu dem imperativen Ansatz, bei dem nachfolgende Deklarationen das dynamische Verhalten zuvor deklarierter Werte ändern können.

Nach meinem Verständnis besteht ein FRP-Programm aus einer Reihe von Gleichungen: Geben Sie hier die Bildbeschreibung ein

j ist diskret: 1,2,3,4 ...

fhängt davon ab, tdass dies die Möglichkeit beinhaltet, externe Reize zu modellieren

Der gesamte Status des Programms ist in Variablen eingekapselt x_i

Die FRP-Bibliothek kümmert sich um die fortschreitende Zeit, mit anderen Worten, jum j+1.

Ich erkläre diese Gleichungen in diesem Video viel detaillierter .

BEARBEITEN:

Ungefähr zwei Jahre nach der ursprünglichen Antwort bin ich kürzlich zu dem Schluss gekommen, dass FRP-Implementierungen einen weiteren wichtigen Aspekt haben. Sie müssen (und tun dies normalerweise) ein wichtiges praktisches Problem lösen: die Ungültigmachung des Caches .

Die Gleichungen für x_i-s beschreiben einen Abhängigkeitsgraphen. Wenn einige der x_iÄnderungen zu einem bestimmten Zeitpunkt vorgenommen werden, müssen jnicht alle anderen x_i'Werte j+1aktualisiert werden, sodass nicht alle Abhängigkeiten neu berechnet werden müssen, da einige x_i'möglicherweise unabhängig von sind x_i.

Darüber hinaus können x_i-s, die sich ändern, schrittweise aktualisiert werden. Zum Beispiel lassen betrachtet die Karte Operation f=g.map(_+1)in Scala, wo fund gsind Listvon Ints. Hier fentspricht x_i(t_j)und gist x_j(t_j). Wenn ich nun ein Element voranstelle g, wäre es verschwenderisch, die mapOperation für alle Elemente in auszuführen g. Einige FRP-Implementierungen (zum Beispiel reflex-frp ) zielen darauf ab, dieses Problem zu lösen. Dieses Problem wird auch als inkrementelles Rechnen bezeichnet.

Mit anderen Worten, Verhaltensweisen (die x_i-s) in FRP können als zwischengespeicherte Berechnungen betrachtet werden. Es ist die Aufgabe der FRP-Engine, diese Cache-s (die x_i-s) effizient ungültig zu machen und neu zu berechnen, wenn sich einige der f_i-s ändern.


4
Ich war genau dort mit dir, bis du mit diskreten Gleichungen gegangen bist . Die Grundidee von FRP war ununterbrochene Zeit , in der es kein " j+1" gibt. Denken Sie stattdessen an Funktionen der kontinuierlichen Zeit. Wie Newton, Leibniz und andere uns gezeigt haben, ist es oft sehr praktisch (und im wahrsten Sinne des Wortes "natürlich"), diese Funktionen unter Verwendung von Integralen und ODE-Systemen differenziell, aber kontinuierlich zu beschreiben. Andernfalls beschreiben Sie einen Approximationsalgorithmus (und einen schlechten) anstelle des Dings selbst.
Conal

Die HTML-Vorlagen und Layoutbeschränkungen der Sprache layx scheinen Elemente von FRP auszudrücken.

@Conal Ich frage mich, wie sich FRP von ODEs unterscheidet. Wie unterscheiden sie sich?
Jhegedus

@jhegedus In dieser Integration (möglicherweise rekursiv, dh ODEs) wird einer der Bausteine ​​von FRP bereitgestellt, nicht die Gesamtheit. Jedes Element des FRP-Vokabulars (einschließlich, aber nicht beschränkt auf Integration) wird in Bezug auf die kontinuierliche Zeit genau erklärt. Hilft diese Erklärung?
Conal


29

Haftungsausschluss: Meine Antwort steht im Zusammenhang mit rx.js - einer Bibliothek für reaktive Programmierung für Javascript.

Bei der funktionalen Programmierung wenden Sie, anstatt jedes Element einer Sammlung zu durchlaufen, Funktionen höherer Ordnung (HoFs) auf die Sammlung selbst an. Die Idee hinter FRP ist also, anstatt jedes einzelne Ereignis zu verarbeiten, einen Strom von Ereignissen zu erstellen (implementiert mit einem beobachtbaren *) und stattdessen HoFs darauf anzuwenden. Auf diese Weise können Sie das System als Daten-Pipelines visualisieren, die Publisher mit Abonnenten verbinden.

Die Hauptvorteile der Verwendung eines Observable sind:
i) Es abstrahiert den Status von Ihrem Code, z. B. wenn Sie möchten, dass der Ereignishandler nur für jedes 'n'-te Ereignis ausgelöst wird oder nach den ersten' n 'Ereignissen nicht mehr ausgelöst wird. Wenn Sie erst nach den ersten 'n' Ereignissen mit dem Auslösen beginnen, können Sie einfach die HoFs (Filter, TakeUntil, Skip) verwenden, anstatt Zähler zu setzen, zu aktualisieren und zu überprüfen.
ii) Es verbessert die Codelokalität. Wenn 5 verschiedene Ereignishandler den Status einer Komponente ändern, können Sie deren Observable zusammenführen und stattdessen einen einzelnen Ereignishandler für das zusammengeführte Observable definieren, wodurch 5 Ereignishandler effektiv zu 1 kombiniert werden. Dies macht es sehr Es ist leicht zu überlegen, welche Ereignisse in Ihrem gesamten System eine Komponente beeinflussen können, da alles in einem einzigen Handler vorhanden ist.

  • Ein Observable ist das Dual eines Iterable.

Ein Iterable ist eine träge konsumierte Sequenz - jedes Element wird vom Iterator gezogen, wann immer es verwendet werden soll, und daher wird die Aufzählung vom Verbraucher gesteuert.

Ein Observable ist eine träge produzierte Sequenz - jedes Element wird an den Beobachter weitergeleitet, wenn es zur Sequenz hinzugefügt wird, und daher wird die Aufzählung vom Produzenten gesteuert.


1
Vielen Dank für diese einfache Definition eines Observablen und seine Unterscheidung von Iterablen. Ich denke, es ist oft sehr hilfreich, ein komplexes Konzept mit seinem bekannten dualen Konzept zu vergleichen, um ein echtes Verständnis zu erlangen.

2
"Die Idee hinter FRP ist also, anstatt jedes einzelne Ereignis zu verarbeiten, einen Strom von Ereignissen zu erstellen (implementiert mit einem beobachtbaren *) und stattdessen HoFs darauf anzuwenden." Ich könnte mich irren, aber ich glaube, dies ist nicht wirklich FRP, sondern eine nette Abstraktion über das Observer-Entwurfsmuster, die funktionale Operationen über HoF ermöglicht (was großartig ist!), Während sie weiterhin mit imperativem Code verwendet werden sollen. Diskussion zum Thema - lambda-the-ultimate.org/node/4982
nqe

18

Alter, das ist eine verdammt geniale Idee! Warum habe ich 1998 nichts davon erfahren? Wie auch immer, hier ist meine Interpretation des Fran- Tutorials. Vorschläge sind sehr willkommen. Ich denke darüber nach, eine darauf basierende Spiel-Engine zu starten.

import pygame
from pygame.surface import Surface
from pygame.sprite import Sprite, Group
from pygame.locals import *
from time import time as epoch_delta
from math import sin, pi
from copy import copy

pygame.init()
screen = pygame.display.set_mode((600,400))
pygame.display.set_caption('Functional Reactive System Demo')

class Time:
    def __float__(self):
        return epoch_delta()
time = Time()

class Function:
    def __init__(self, var, func, phase = 0., scale = 1., offset = 0.):
        self.var = var
        self.func = func
        self.phase = phase
        self.scale = scale
        self.offset = offset
    def copy(self):
        return copy(self)
    def __float__(self):
        return self.func(float(self.var) + float(self.phase)) * float(self.scale) + float(self.offset)
    def __int__(self):
        return int(float(self))
    def __add__(self, n):
        result = self.copy()
        result.offset += n
        return result
    def __mul__(self, n):
        result = self.copy()
        result.scale += n
        return result
    def __inv__(self):
        result = self.copy()
        result.scale *= -1.
        return result
    def __abs__(self):
        return Function(self, abs)

def FuncTime(func, phase = 0., scale = 1., offset = 0.):
    global time
    return Function(time, func, phase, scale, offset)

def SinTime(phase = 0., scale = 1., offset = 0.):
    return FuncTime(sin, phase, scale, offset)
sin_time = SinTime()

def CosTime(phase = 0., scale = 1., offset = 0.):
    phase += pi / 2.
    return SinTime(phase, scale, offset)
cos_time = CosTime()

class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
    @property
    def size(self):
        return [self.radius * 2] * 2
circle = Circle(
        x = cos_time * 200 + 250,
        y = abs(sin_time) * 200 + 50,
        radius = 50)

class CircleView(Sprite):
    def __init__(self, model, color = (255, 0, 0)):
        Sprite.__init__(self)
        self.color = color
        self.model = model
        self.image = Surface([model.radius * 2] * 2).convert_alpha()
        self.rect = self.image.get_rect()
        pygame.draw.ellipse(self.image, self.color, self.rect)
    def update(self):
        self.rect[:] = int(self.model.x), int(self.model.y), self.model.radius * 2, self.model.radius * 2
circle_view = CircleView(circle)

sprites = Group(circle_view)
running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
        if event.type == KEYDOWN and event.key == K_ESCAPE:
            running = False
    screen.fill((0, 0, 0))
    sprites.update()
    sprites.draw(screen)
    pygame.display.flip()
pygame.quit()

Kurz gesagt: Wenn jede Komponente wie eine Zahl behandelt werden kann, kann das gesamte System wie eine mathematische Gleichung behandelt werden, oder?


1
Das ist ein bisschen spät, aber trotzdem ... Frag ist ein Spiel mit FRP .
Arx

14

Paul Hudaks Buch The Haskell School of Expression ist nicht nur eine gute Einführung in Haskell, sondern verbringt auch viel Zeit mit FRP. Wenn Sie ein Anfänger mit FRP sind, empfehle ich es sehr, um Ihnen einen Eindruck davon zu vermitteln, wie FRP funktioniert.

Es gibt auch eine neue Neufassung dieses Buches (veröffentlicht 2011, aktualisiert 2014), The Haskell School of Music .


10

Nach den vorherigen Antworten scheint es, dass wir mathematisch einfach in einer höheren Ordnung denken. Anstatt an einen Wert x vom Typ X zu denken, denken wir an eine Funktion x : TX , wobei T die Art der Zeit ist, sei es die natürlichen Zahlen, die ganzen Zahlen oder das Kontinuum. Wenn wir nun y : = x + 1 in der Programmiersprache schreiben , meinen wir tatsächlich die Gleichung y ( t ) = x ( t ) + 1.


9

Verhält sich wie eine Tabelle wie angegeben. In der Regel basierend auf einem ereignisgesteuerten Framework.

Wie bei allen "Paradigmen" ist die Neuheit umstritten.

Aus meiner Erfahrung mit verteilten Flussnetzwerken von Akteuren kann es leicht zu einem allgemeinen Problem der Zustandskonsistenz im Netzwerk von Knoten kommen, dh Sie haben viel Schwingung und werden in seltsamen Schleifen gefangen.

Dies ist schwer zu vermeiden, da einige Semantiken Referenzschleifen oder Rundfunk implizieren und ziemlich chaotisch sein können, wenn das Netzwerk von Akteuren in einem unvorhersehbaren Zustand konvergiert (oder nicht).

In ähnlicher Weise können einige Zustände trotz genau definierter Kanten nicht erreicht werden, da der globale Zustand von der Lösung abweicht. 2 + 2 kann 4 werden oder nicht, abhängig davon, wann die 2 zu 2 wurden und ob sie so geblieben sind. Tabellenkalkulationen verfügen über synchrone Uhren und Schleifenerkennung. Verteilte Schauspieler tun dies im Allgemeinen nicht.

Alles viel Spaß :).



7

Dieser Artikel von Andre Staltz ist die beste und klarste Erklärung, die ich bisher gesehen habe.

Einige Zitate aus dem Artikel:

Reaktive Programmierung ist die Programmierung mit asynchronen Datenströmen.

Darüber hinaus erhalten Sie eine erstaunliche Toolbox mit Funktionen zum Kombinieren, Erstellen und Filtern dieser Streams.

Hier ist ein Beispiel für die fantastischen Diagramme, die Teil des Artikels sind:

Klicken Sie auf Ereignisstromdiagramm


5

Es geht um mathematische Datentransformationen über die Zeit (oder das Ignorieren der Zeit).

Im Code bedeutet dies funktionale Reinheit und deklarative Programmierung.

Staatliche Fehler sind ein großes Problem im Standard-Imperativ-Paradigma. Verschiedene Codebits können einen gemeinsamen Zustand zu unterschiedlichen "Zeiten" in der Programmausführung ändern. Das ist schwer zu bewältigen.

In FRP beschreiben Sie (wie in der deklarativen Programmierung), wie Daten von einem Zustand in einen anderen umgewandelt werden und was sie auslöst. Auf diese Weise können Sie die Zeit ignorieren, da Ihre Funktion einfach auf ihre Eingaben reagiert und ihre aktuellen Werte verwendet, um eine neue zu erstellen. Dies bedeutet, dass der Zustand im Diagramm (oder Baum) der Transformationsknoten enthalten ist und funktional rein ist.

Dies reduziert die Komplexität und die Debugging-Zeit massiv.

Denken Sie an den Unterschied zwischen A = B + C in Mathematik und A = B + C in einem Programm. In der Mathematik beschreiben Sie eine Beziehung, die sich nie ändern wird. In einem Programm heißt es, dass "Im Moment" A B + C ist. Der nächste Befehl könnte jedoch B ++ sein. In diesem Fall ist A nicht gleich B + C. In der mathematischen oder deklarativen Programmierung ist A immer gleich B + C, egal zu welchem ​​Zeitpunkt Sie fragen.

Entfernen Sie also die Komplexität des gemeinsamen Zustands und ändern Sie die Werte im Laufe der Zeit. Ihr Programm ist viel einfacher zu überlegen.

Ein EventStream ist ein EventStream + eine Transformationsfunktion.

Ein Verhalten ist ein EventStream + Ein Wert im Speicher.

Wenn das Ereignis ausgelöst wird, wird der Wert durch Ausführen der Transformationsfunktion aktualisiert. Der Wert, den dies erzeugt, wird im Verhaltensspeicher gespeichert.

Verhaltensweisen können zusammengesetzt werden, um neue Verhaltensweisen zu erzeugen, die eine Transformation für N andere Verhaltensweisen darstellen. Dieser zusammengesetzte Wert wird neu berechnet, wenn die Eingabeereignisse (Verhaltensweisen) ausgelöst werden.

"Da Beobachter zustandslos sind, benötigen wir häufig mehrere von ihnen, um eine Zustandsmaschine wie im Drag-Beispiel zu simulieren. Wir müssen den Zustand speichern, in dem er für alle beteiligten Beobachter zugänglich ist, wie im obigen variablen Pfad."

Zitat aus - Verwerfen des Beobachtermusters http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf


Genau so fühle ich mich bei der deklarativen Programmierung, und Sie beschreiben die Idee einfach besser als ich.
Neevek

2

Die kurze und klare Erklärung zur reaktiven Programmierung finden Sie in Cyclejs - Reaktive Programmierung . Sie verwendet einfache und visuelle Beispiele.

Ein [Modul / Komponente / Objekt] ist reaktiv dh es ist voll verantwortlich für die Verwaltung seines eigenen Zustands, indem es auf externe Ereignisse reagiert.

Was ist der Vorteil dieses Ansatzes? Es ist die Umkehrung der Kontrolle , hauptsächlich weil [Modul / Komponente / Objekt] für sich selbst verantwortlich ist und die Kapselung mit privaten Methoden gegen öffentliche verbessert.

Es ist ein guter Startpunkt, keine vollständige Wissensquelle. Von dort aus konnte man zu komplexeren und tieferen Papieren springen.


0

Schauen Sie sich Rx, Reactive Extensions für .NET an. Sie weisen darauf hin, dass Sie mit IEnumerable im Grunde genommen aus einem Stream "ziehen". Linq-Abfragen über IQueryable / IEnumerable sind Set-Operationen, die die Ergebnisse aus einem Set "saugen". Mit denselben Operatoren über IObservable können Sie jedoch Linq-Abfragen schreiben, die "reagieren".

Sie können beispielsweise eine Linq-Abfrage wie (von m in MyObservableSetOfMouseMovements, wobei mX <100 und mY <100 einen neuen Punkt (mX, mY) auswählen) schreiben.

und mit den Rx-Erweiterungen ist es soweit: Sie haben UI-Code, der auf den eingehenden Strom von Mausbewegungen reagiert und zeichnet, wann immer Sie sich in der 100.100-Box befinden ...


0

FRP ist eine Kombination aus funktionaler Programmierung (Programmierparadigma, das auf der Idee basiert, dass alles eine Funktion ist) und reaktivem Programmierparadigma (basierend auf der Idee, dass alles ein Strom ist (Beobachter und beobachtbare Philosophie)). Es soll das Beste der Welt sein.

Schauen Sie sich zunächst den Beitrag von Andre Staltz zur reaktiven Programmierung an.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.