functional programming - vorteile - Was ist(funktionale) reaktive Programmierung?




rxjs (12)

Ich habe den Wikipedia-Artikel über reaktive Programmierung gelesen. Ich habe auch den kleinen Artikel über die funktionale reaktive Programmierung gelesen. 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.


Überprüfen Sie Rx, Reaktive Erweiterungen für .NET. Sie weisen darauf hin, dass Sie bei IEnumerable grundsätzlich von einem Stream "ziehen". Linq-Abfragen über IQueryable / IEnumerable sind gesetzte Operationen, die die Ergebnisse aus einer Menge "aussaugen". Aber mit den gleichen Operatoren über IObservable können Sie Linq-Abfragen schreiben, die 'reagieren'.

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

und mit den Rx-Erweiterungen, das ist es: Sie haben UI-Code, der auf den eingehenden Strom von Mausbewegungen reagiert und zeichnet, wann immer Sie in der 100,100-Box sind ...


Alter, das ist eine verdammt gute Idee! Warum habe ich das 1998 nicht herausgefunden? Wie auch immer, hier ist meine Interpretation des Fran Tutorials. Vorschläge sind sehr willkommen, ich denke darüber nach, auf dieser Grundlage eine 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 ganze System wie eine mathematische Gleichung behandelt werden, richtig?


Die kurze und klare Erklärung zur Reaktiven Programmierung wird auf Cyclejs - Reactive Programming angezeigt , sie verwendet einfache und visuelle Beispiele.

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

Was ist der Vorteil dieses Ansatzes? Es ist eine Inversion 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 konnte man zu komplexeren und tiefgründigeren Papieren springen.


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, was effektiv 5 Event-Handler zu 1 kombiniert 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 lazy produzierte Sequenz - jedes Element wird zum Beobachter geschoben, wenn es der Sequenz hinzugefügt wird, und daher wird die Enumeration vom Produzenten gesteuert.


Es handelt sich um mathematische Datentransformationen über die Zeit (oder um die Zeit zu ignorieren).

Im Code bedeutet das funktionale Reinheit und deklarative Programmierung.

Zustandsfehler sind ein großes Problem im Standardimperativ-Paradigma. Verschiedene Code-Bits können einen gemeinsamen Zustand zu verschiedenen "Zeiten" in der Programmausführung ändern. Das ist schwer zu bewältigen.

In FRP beschreiben Sie (wie bei der deklarativen Programmierung), wie Daten von einem Zustand in einen anderen transformiert werden und was diesen auslöst. Dadurch 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 in dem Graphen (oder Baum) von Transformationsknoten enthalten ist und funktional rein ist.

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

Denken Sie an den Unterschied zwischen A = B + C in Mathematik und A = B + C in einem Programm. In Mathe beschreibst du eine Beziehung, die sich niemals ändern wird. In einem Programm sagt es, dass "gerade jetzt" A B + C ist. Aber der nächste Befehl könnte B ++ sein, in welchem ​​Fall A nicht gleich B + C ist. In Mathematik oder deklarativer Programmierung wird A immer gleich B + C sein, egal zu welchem ​​Zeitpunkt Sie fragen.

Also, indem Sie die Komplexitäten von gemeinsamem Status und sich ändernden Werten im Laufe der Zeit entfernen. Ihr Programm ist viel einfacher zu verstehen.

Ein EventStream ist ein EventStream + eine Transformationsfunktion.

Ein Verhalten ist ein EventStream + ein gewisser Wert im Speicher.

Wenn das Ereignis ausgelöst wird, wird der Wert aktualisiert, indem die Transformationsfunktion ausgeführt wird. Der Wert, den dies erzeugt, wird im Verhaltensspeicher gespeichert.

Verhaltensweisen können zusammengesetzt werden, um neue Verhaltensweisen zu erzeugen, die eine Transformation von N anderen Verhaltensweisen sind. Dieser zusammengesetzte Wert wird neu berechnet, wenn die Eingabeereignisse (Verhalten) ausgelöst werden.

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

Zitat von - Vernachlässigung des Beobachtermusters http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf


Für mich sind es ungefähr 2 verschiedene Bedeutungen von symbol = :

  1. In Mathe bedeutet x = sin(t) , dass x ein anderer Name für sin(t) . Also ist das Schreiben von x + y dasselbe wie sin(t) + y . Funktionale reaktive Programmierung ist in dieser Hinsicht wie Mathe: Wenn Sie x + y schreiben, wird es mit dem Wert von t berechnet, der zu der Zeit verwendet wird, zu der es verwendet wird.
  2. In C-ähnlichen Programmiersprachen (Imperativsprachen) ist x = sin(t) eine Zuweisung: es bedeutet, dass x den Wert von sin(t) speichert, der zum Zeitpunkt der Zuweisung genommen wurde.


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, haben ihrerseits 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 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 Variablen x und y 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 ist minX immer 16 kleiner als die x-Koordinate des Mauszeigers. 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 gutes Referat über funktionale reaktive Programmierung .


Nachdem ich viele Seiten über FRP gelesen hatte, bin ich endlich auf this aufschlussreiche Schreiben über FRP gestoßen, das mich endlich dazu gebracht hat zu verstehen, 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 in Form von zeitveränderlichen Funktionen anstatt eines veränderlichen Zustands zu beschreiben ", und das wäre sicherlich nicht falsch. Dies ist der semantische Standpunkt. Aber meiner Meinung nach ist die tiefere, befriedigendere Antwort das folgende rein syntaktische Kriterium:

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

Nehmen wir als Beispiel einen Zähler: Sie haben zwei Tasten, die mit "Up" und "Down" beschriftet sind und zum Erhöhen oder Verringern des Zählers verwendet werden können. Unbedingt sollten Sie zuerst einen Anfangswert angeben und ihn dann ändern, wenn eine Schaltfläche 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 restlichen Programmtext 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 eines Zählers verstehen wollen, müssen Sie nur auf seine Definition schauen. Alles, was ihm passieren kann, erscheint auf der rechten Seite. Dies steht im Gegensatz zu dem imperativen Ansatz, bei dem nachfolgende Deklarationen das dynamische Verhalten zuvor deklarierter Werte ändern können.

Also, nach meinem Verständnis ist ein FRP-Programm eine Menge von Gleichungen:

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

f hängt von t also beinhaltet dies die Möglichkeit, externe Stimuli zu modellieren

Der gesamte Status des Programms ist in Variablen x_i eingekapselt

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

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

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-Ungültigkeit .

Die Gleichungen für x_i -s beschreiben einen Abhängigkeitsgraphen. Wenn sich einige der x_i zum Zeitpunkt j ändern, müssen nicht alle anderen x_i' Werte bei j+1 aktualisiert werden, so dass nicht alle Abhängigkeiten neu berechnet werden müssen, da einige x_i' unabhängig von x_i .

Außerdem können x_i -s, die sich ändern, inkrementell aktualisiert werden. Betrachten wir zum Beispiel eine f=g.map(_+1) in Scala, wobei f und g die List der Ints . Hier entspricht f x_i(t_j) und g ist x_j(t_j) . Wenn ich jetzt ein Element an g anschließe, wäre es verschwenderisch, die map für alle Elemente in g auszuführen. Einige FRP-Implementierungen (z. B. reflex-frp ) zielen darauf ab, dieses Problem zu lösen. Dieses Problem wird auch als inkrementelles Computing bezeichnet.

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


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 Musikwiedergabeanwendung) 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 bekanntes Beispiel war die Manchester Dataflow Machine , die (IIRC) eine Architektur mit markierten Daten verwendete, um die Ausführung von Knoten im Graphen über 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 den Geist der Sache betrachtet, ist ein Programm, das eine Ereigniswarteschlange in einem festen Intervall abfragt und alle gefundenen Ereignisse an Funktionen (oder Threads) absetzt, weniger reaktiv (weil es nur die Benutzereingabe in einem festen Intervall behandelt). 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.


Wirkt wie eine Kalkulationstabelle wie angegeben. Normalerweise basierend auf einem ereignisgesteuerten Framework.

Wie bei allen "Paradigmen" ist seine Neuheit umstritten.

Aus meiner Erfahrung mit verteilten Flussnetzen von Akteuren kann es leicht zu einem allgemeinen Problem der Zustandskonsistenz über das Netzwerk von Knotenpunkten kommen, dh Sie enden mit einer Menge Oszillation und Einfangen in seltsamen Schleifen.

Dies ist schwer zu vermeiden, da einige Semantiken referenzielle Schleifen oder Broadcasts 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 gut definierter Kanten nicht erreicht werden, weil der globale Zustand von der Lösung weg steuert. 2 + 2 kann oder darf nicht 4 werden, abhängig davon, wann die 2er 2 wurden und ob sie so blieben. Spreadsheets haben synchrone Uhren und Loop-Erkennung. Verteilte Akteure tun dies im Allgemeinen nicht.

Alles gute Spaß :).


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 das Programmieren mit asynchronen Datenströmen.

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

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







frp