Frage Was ist (funktionale) reaktive Programmierung?


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

  1. Was bedeutet funktionale reaktive Programmierung (FRP) in der Praxis?
  2. Was beinhaltet die reaktive Programmierung (im Gegensatz zur nicht reaktiven Programmierung?)?

Mein Hintergrund ist in Imperativ / OO-Sprachen, so dass eine Erklärung, die sich auf dieses Paradigma bezieht, geschätzt wird.


1149
2018-06-22 16:41


Ursprung


Antworten:


Wenn Sie ein Gefühl für FRP bekommen möchten, könnten Sie mit dem Alten beginnen Fran Tutorial von 1998, die animierte Illustrationen hat. Beginnen Sie mit Papieren mit Funktionale reaktive Animation und dann folgen Sie den Links auf den Publikationen Link auf meiner Homepage und der FRP Link auf der Haskell Wiki.

Persönlich denke ich gerne darüber nach, was FRP ist meint bevor Sie darauf eingehen, wie es implementiert werden könnte. (Code ohne Spezifikation ist eine Antwort ohne Frage und somit "nicht einmal falsch".) Daher beschreibe ich FRP nicht in Repräsentations- / Implementierungsbegriffen, wie Thomas K in einer anderen Antwort (Graphen, Knoten, Kanten, Feuern, Ausführung usw.). Es gibt viele mögliche Implementierungsstile, aber keine Implementierung sagt was FRP ist.

Ich stimme mit der einfachen Beschreibung von Laurence G überein, dass es bei FRP um "Datentypen geht, die im Laufe der Zeit einen Wert darstellen". Die konventionelle imperative Programmierung erfasst diese dynamischen Werte nur indirekt über Zustände und Mutationen. Die vollständige Geschichte (Vergangenheit, Gegenwart, Zukunft) hat keine erstklassige Repräsentation. Außerdem nur sich diskret entwickelnd Werte können (indirekt) erfasst werden, da das Imperativ-Paradigma zeitlich diskret ist. Im Gegensatz dazu erfasst FRP diese sich entwickelnden Werte direkt und hat keine Schwierigkeiten mit ständig sich entwickelnde Werte.

FRP ist auch insofern ungewöhnlich, als es gleichzeitig ist, ohne mit dem theoretischen und pragmatischen Rattennest in Konflikt zu geraten, das zwingende Nebenläufigkeit fordert. Semantisch ist FRPs Nebenläufigkeit feinkörnig, bestimmt, und kontinuierlich. (Ich spreche über Bedeutung, nicht über Implementierung. Eine Implementierung kann Nebenläufigkeit oder Parallelität beinhalten oder nicht). Semantische Determiniertheit ist sehr wichtig für das Argumentieren, sowohl rigoros als auch informell. Während Nebenläufigkeit der imperativen Programmierung eine enorme Komplexität hinzufügt (aufgrund von nicht deterministischem Interleaving), ist es in FRP mühelos möglich.

Also, was ist FRP? Du hättest es selbst erfunden. Beginne mit diesen Ideen:

  • Dynamische / sich entwickelnde Werte (d. H. Werte "über die Zeit") sind selbst erste Klassenwerte. Sie können sie definieren und kombinieren, sie in und aus Funktionen übergeben. Ich nannte diese Dinge "Verhaltensweisen".

  • Verhaltensweisen werden aus ein paar Primitiven aufgebaut, wie etwa konstantes (statisches) Verhalten und Zeit (wie eine Uhr) und dann mit sequenzieller und paralleler Kombination. n Verhaltensweisen werden kombiniert, indem eine n-artige Funktion (auf statische Werte) "punktweise" angewendet wird, d. h. kontinuierlich über die Zeit.

  • Um diskrete Phänomene zu berücksichtigen, haben Sie einen anderen Typ (Familie) von "Ereignissen", von denen jedes einen Strom (endliche oder unendliche) von Ereignissen hat. Jedem Ereignis sind Zeit und Wert zugeordnet.

  • Um mit dem kompositorischen Vokabular, aus dem alle Verhaltensweisen und Ereignisse hervorgehen, zu kommen, spielen Sie mit einigen Beispielen. Dekonstruieren Sie weiter in allgemeinere / einfachere Teile.

  • Damit Sie wissen, dass Sie auf einer soliden Grundlage sind, geben Sie dem gesamten Modell eine kompositorische Grundlage, indem Sie die Technik der Denotationssemantik verwenden, was nur bedeutet, dass (a) jeder Typ einen entsprechenden einfachen & präzisen mathematischen Typ von "Bedeutungen" hat und ( b) jedes Primitiv und jeder Operator hat eine einfache und präzise Bedeutung als Funktion der Bedeutungen der Bestandteile. Niemals mische Überlegungen zur Umsetzung in deinen Explorationsprozess ein. Wenn diese Beschreibung für Sie ein Kauderwelsch ist, konsultieren Sie (a) Denotationales Design mit Typklassenmorphismen(b) Gegentaktfunktionale reaktive Programmierung (Ignorieren der Implementierungsbits) und (c) der Denotationale Semantik Haskell wikibooks Seite. Beachten Sie, dass die Denotationssemantik zwei Teile hat, von ihren beiden Gründern Christopher Strachey und Dana Scott: der leichtere und nützlichere Strachey-Part und der härtere und weniger nützliche (für Software-Design) Scott-Part.

Wenn Sie sich an diese Prinzipien halten, erwarte ich, dass Sie etwas mehr oder weniger im Sinne von FRP bekommen.

Woher habe ich diese Prinzipien? Beim Softwaredesign stelle ich immer die gleiche Frage: "Was bedeutet das?". Die Denotationssemantik gab mir einen präzisen Rahmen für diese Frage, und zwar eine, die meiner Ästhetik entspricht (im Gegensatz zur operationalen oder axiomatischen Semantik, die mich beide unzufrieden machen). Also habe ich mich gefragt, was ist Verhalten? Ich erkannte bald, dass die zeitlich diskrete Natur der imperativen Berechnung eine Anpassung an einen bestimmten Stil von ist Maschine, anstatt eine natürliche Beschreibung des Verhaltens selbst. 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 eine kontinuierliche, deterministische Gleichzeitigkeit mit Leichtigkeit und Anmut.

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


932
2018-06-23 04:31



In der reinen funktionalen Programmierung gibt es keine Nebenwirkungen. Für viele Arten von Software (z. B. alles mit Benutzerinteraktion) sind Nebenwirkungen auf einer bestimmten Ebene notwendig.

Eine Möglichkeit, ein Nebeneffekt-ähnliches Verhalten zu erhalten, während ein funktionaler Stil beibehalten wird, ist die Verwendung einer funktionalen reaktiven Programmierung. Dies ist die Kombination aus funktionaler Programmierung und reaktiver Programmierung. (Der Wikipedia-Artikel, mit dem Sie verlinkt haben, handelt von Letzterem.)

Die Grundidee der reaktiven Programmierung besteht darin, dass bestimmte Datentypen einen Wert "über die Zeit" repräsentieren. Berechnungen, die diese Werte für die Änderung der Zeit beinhalten, werden selbst Werte haben, 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 hätten etwas (Pseudo-Code):

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 Zuweisung 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: Reaktive Programmierung macht es überflüssig, Variablen zu mutieren, während Sie immer noch vieles tun können, was Sie mit variablen Mutationen erreichen könnten.

Wenn wir dann einige Berechnungen basierend darauf durchführen, werden die resultierenden Werte auch Werte sein, die sich im Laufe der Zeit ändern. Beispielsweise:

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

In diesem Beispiel minX wird immer 16 weniger als die X-Koordinate des Mauszeigers sein. Mit reaktiven Bibliotheken könnte man dann sagen:

rectangle(minX, minY, maxX, maxY)

Und eine 32x32-Box wird um den Mauszeiger gezogen und verfolgt sie überall dort, wo sie sich bewegt.

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


740
2018-06-22 18:06



Eine einfache Möglichkeit, eine erste Intuition darüber zu erreichen, wie es ist, ist, sich vorzustellen, dass Ihr Programm eine Kalkulationstabelle ist und alle Ihre Variablen Zellen sind. Wenn sich eine der Zellen in einer Tabelle ändert, ändern sich auch alle Zellen, die sich auf diese Zelle beziehen. Bei FRP ist es genauso. Stellen Sie sich nun vor, dass sich einige der Zellen von selbst ändern (oder vielmehr von der Außenwelt): In einer GUI-Situation wäre die Position der Maus ein gutes Beispiel.

Das geht sehr viel aus. Die Metapher bricht ziemlich schnell zusammen, wenn Sie tatsächlich ein FRP-System verwenden. Zum einen gibt es üblicherweise Versuche, auch diskrete Ereignisse zu modellieren (z. B. die Maus, auf die geklickt wird). Ich mache das hier nur, um Ihnen eine Idee zu geben, wie es ist.


144
2018-06-23 14:52



Für mich sind es zwei verschiedene Bedeutungen des Symbols =:

  1. In Mathe x = sin(t) bedeutet, dass x ist anderer Name zum sin(t). Also schreiben x + y ist das Gleiche wie sin(t) + y. Funktionale reaktive Programmierung ist in dieser Hinsicht wie Mathe: wenn Sie schreiben x + yEs wird mit dem Wert von berechnet t ist zu der Zeit es benutzt wird.
  2. In C-ähnlichen Programmiersprachen (Imperative Sprachen), x = sin(t) ist eine Aufgabe: Es bedeutet, dass x speichert die Wert von  sin(t) zum Zeitpunkt des Auftrags genommen.

132
2018-05-25 14:52



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

Dies ist ziemlich gut für UI-Design geeignet, zum Beispiel, wenn das Berühren einer Benutzerschnittstellensteuerung (beispielsweise der Lautstärkeregler bei einer Musik spielenden Anwendung) verschiedene Anzeigeelemente und die tatsächliche Lautstärke der Audioausgabe aktualisieren muss. Wenn Sie die Lautstärke ändern (z. B. einen Schieberegler), würde das dem Ändern des Werts entsprechen, der einem Knoten in einem gerichteten Diagramm zugeordnet ist.

Verschiedene Knoten, die Kanten von diesem "Volumenwert" -Knoten haben, würden automatisch ausgelöst werden, und alle notwendigen Berechnungen und Aktualisierungen würden sich natürlich durch die Anwendung verteilen. Die Anwendung "reagiert" auf den Benutzerreiz. Funktionale reaktive Programmierung wäre nur die Implementierung dieser Idee in einer funktionalen Sprache oder allgemein in einem funktionalen Programmierparadigma.

Um mehr über "Datenfluss-Computing" zu erfahren, suchen Sie auf Wikipedia nach diesen beiden Wörtern oder verwenden Sie Ihre bevorzugte Suchmaschine. Die allgemeine Idee ist dies: Das Programm ist ein gerichteter Graph von Knoten, von denen jeder eine einfache Berechnung durchführt. Diese Knoten sind über Graph-Links miteinander verbunden, die die Ausgänge einiger Knoten den Eingängen anderer Knoten zur Verfügung stellen.

Wenn ein Knoten zündet oder seine Berechnung durchführt, haben die Knoten, die mit seinen Ausgängen verbunden sind, ihre entsprechenden Eingänge "ausgelöst" oder "markiert". Jeder Knoten, der alle Eingänge ausgelöst / markiert / verfügbar hat, wird automatisch ausgelöst. Der Graph kann implizit oder explizit sein, abhängig davon, wie genau die reaktive Programmierung implementiert ist.

Knoten können als parallel feuern betrachtet werden, aber oft werden sie seriell oder mit begrenzter Parallelität ausgeführt (zum Beispiel können einige Threads sie ausführen). Ein berühmtes Beispiel war das Manchester Dataflow-Maschine, die (IIRC) eine markierte Datenarchitektur verwendete, um die Ausführung von Knoten in dem Graphen durch eine oder mehrere Ausführungseinheiten zu planen. Datenflussberechnung ist ziemlich gut für Situationen geeignet, in denen das Auslösen von Berechnungen, die asynchron zu Kaskaden von Berechnungen führen, besser funktioniert, als zu versuchen, die Ausführung durch eine Uhr (oder Uhren) zu steuern.

Reaktive Programmierung importiert diese "Kaskade der Ausführung" Idee und scheint das Programm in einer datenflussartigen Weise zu denken, aber unter der Voraussetzung, dass einige der Knoten an die "Außenwelt" angeschlossen sind und die Kaskaden der Ausführung ausgelöst werden, wenn diese sensorisch sind Knoten ändern sich. Die Programmausführung würde dann ähnlich aussehen wie ein komplexer Reflexbogen. Das Programm kann im Wesentlichen zwischen Stimuli sitzen oder nicht oder kann sich in einem im Wesentlichen sitzenden Zustand zwischen Stimuli befinden.

Eine "nicht-reaktive" Programmierung würde eine Programmierung mit einer sehr unterschiedlichen Ansicht des Ausführungsablaufs und der Beziehung zu externen Eingaben sein. Es ist wahrscheinlich etwas subjektiv, da die Leute wahrscheinlich versucht sind, irgendetwas zu sagen, das auf externe Eingaben reagiert und auf sie "reagiert". Aber wenn man sich den Geist der Sache ansieht, ist ein Programm, das eine Ereigniswarteschlange in einem festen Intervall abfragt und alle gefundenen Ereignisse an Funktionen (oder Threads) absetzt, weniger reaktiv (weil es sich nur um Benutzereingaben in einem festen Intervall kümmert). Auch hier ist es der Geist der Sache: Man kann sich vorstellen, eine Polling-Implementierung mit einem schnellen Polling-Intervall sehr niedrig in ein System zu packen und darauf reaktiv zu programmieren.


71
2018-06-22 17:45



Nachdem ich viele Seiten über FRP gelesen hatte, kam ich endlich dazu Dies Aufschlussreiches über FRP zu schreiben, hat mich endlich verstanden, worum es bei FRP wirklich geht.

Ich zitiere Heinrich Apfelmus (Autor der reaktiven Banane).

Was ist die Essenz der funktionalen reaktiven Programmierung?

Eine allgemeine Antwort wäre, dass es bei FRP darum geht, ein System zu beschreiben   Begriffe der zeitveränderlichen Funktionen anstelle des veränderlichen Zustandes ", und das   wäre sicher nicht falsch. Dies ist der semantische Standpunkt. Aber in   Meiner Meinung nach ist die tiefere, befriedigendere Antwort gegeben durch die   nach rein syntaktischem Kriterium:

Das Wesen der funktionalen reaktiven Programmierung besteht darin, das dynamische Verhalten eines Wertes vollständig zum Zeitpunkt der Deklaration anzugeben.

Nehmen wir zum Beispiel das Beispiel eines Zählers: Sie haben zwei Knöpfe   beschriftet "Up" und "Down", mit denen erhöht oder verringert werden kann   der Zähler. Imperativ würden Sie zuerst einen Anfangswert angeben   und dann ändern Sie es immer, 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 ist   für den Zähler ist angegeben; das dynamische Verhalten des Zählers ist   implizit im Rest des Programmtextes. Im Gegensatz dazu funktional   Reaktive Programmierung spezifiziert das gesamte dynamische Verhalten zu der Zeit   der Erklärung, so:

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

Wann immer Sie die Dynamik von Counter verstehen wollen, haben Sie nur   um seine Definition zu sehen. Alles was damit passieren kann wird   erscheinen auf der rechten Seite. Dies steht sehr im Gegensatz zu der   zwingender Ansatz, bei dem nachfolgende Erklärungen die   dynamisches Verhalten zuvor deklarierter Werte.

Also, in mein Verständnis Ein FRP-Programm ist ein Satz von Gleichungen: enter image description here

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

f kommt drauf an t Dies beinhaltet die Möglichkeit, externe Reize zu modellieren

Der gesamte Status des Programms ist in Variablen gekapselt x_i

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

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

BEARBEITEN:

Ungefähr 2 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 meist tun es) ein wichtiges praktisches Problem lösen: Cache-Invalidierung.

Die Gleichungen für x_i-s beschreibt ein Abhängigkeitsdiagramm. Wenn einige der x_i ändert sich zur Zeit j dann nicht alle anderen x_i' Werte bei j+1 müssen aktualisiert werden, damit nicht alle Abhängigkeiten neu berechnet werden müssen x_i' könnte unabhängig sein von x_i.

Außerdem, x_i-s, die sich ändern, können inkrementell aktualisiert werden. Betrachten wir zum Beispiel eine Kartenoperation f=g.map(_+1) in Scala, wo f und g sind List von Ints. Hier f entspricht x_i(t_j) und g ist x_j(t_j). Nun, wenn ich ein Element vorstelle g dann wäre es verschwenderisch, das durchzuführen map Operation für alle Elemente in g. Einige FRP-Implementierungen (z. B. reflex-frp) zielen darauf ab, dieses Problem zu lösen. Dieses Problem ist auch bekannt als inkrementelles Computing.

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


65
2018-01-31 03:46



Disclaimer: Meine Antwort liegt im Kontext von rx.js - einer reaktiven Programmierbibliothek für Javascript.

In der funktionalen Programmierung wenden Sie statt der einzelnen Elemente einer Sammlung Funktionen höherer Ordnung (HoFs) auf die Sammlung selbst an. Die Idee hinter FRP ist also, dass anstatt jedes einzelne Ereignis zu verarbeiten, ein Strom von Ereignissen (implementiert mit einem beobachtbaren *) erzeugt wird und stattdessen HoFs darauf angewendet werden. Auf diese Weise können Sie das System als Datenpipelines visualisieren, die Publisher mit Abonnenten verbindet.

Die Hauptvorteile der Verwendung eines Observablen sind:
i) es abstrahiert den Status von Ihrem Code, zB wenn Sie möchten, dass der Event-Handler nur für jedes 'n'te Ereignis ausgelöst wird oder nach den ersten' n 'Ereignissen aufhört zu feuern oder erst nach dem ersten' n 'zu feuern beginnt 'Ereignisse können Sie die HoFs (filter, takeUntil, resp. skip) verwenden, anstatt Zähler zu setzen, zu aktualisieren und zu überprüfen.
ii) Es verbessert die Codelokalität - Wenn Sie fünf verschiedene Event-Handler haben, die den Status einer Komponente ändern, können Sie ihre Observablen zusammenführen und stattdessen einen einzelnen Event-Handler für die kombinierte Observable definieren, wodurch effektiv 5 Event-Handler zu 1 kombiniert werden Sie können leicht nachvollziehen, welche Ereignisse in Ihrem gesamten System eine Komponente beeinflussen können, da sie alle in einem einzigen Handler vorhanden sind.

  • Ein Observable ist das Duale eines Iterablen.

Ein Iterable ist eine träge konsumierte Sequenz - jedes Item wird vom Iterator gezogen, wann immer es es benutzen will, und daher wird die Enumeration vom Consumer gesteuert.

Ein Observable ist eine träge erzeugte Sequenz - jedes Element wird zum Beobachter geschoben, wenn es der Sequenz hinzugefügt wird, und daher wird die Enumeration vom Produzenten gesteuert.


30
2018-05-26 17:10