Cos'è la programmazione (funzionale) reattiva?


1148

Ho letto l'articolo di Wikipedia sulla programmazione reattiva . Ho anche letto il piccolo articolo sulla programmazione reattiva funzionale . Le descrizioni sono piuttosto astratte.

  1. Cosa significa in pratica la programmazione reattiva funzionale (FRP)?
  2. In cosa consiste la programmazione reattiva (anziché la programmazione non reattiva?)?

Il mio background è in linguaggi imperativi / OO, quindi una spiegazione relativa a questo paradigma sarebbe apprezzata.


159
ecco un ragazzo con un'immaginazione attiva e buone capacità narrative che prendono tutto. paulstovell.com/reactive-programming
melaos

39
Qualcuno ha davvero bisogno di scrivere una "Programmazione reattiva funzionale per i manichini" per tutti noi autodidatti qui. Ogni risorsa che ho trovato, persino Elm, sembra presumere che tu abbia ottenuto un Master in CS negli ultimi cinque anni. Coloro che conoscono la FRP sembrano aver perso completamente la capacità di vedere la questione dal punto di vista ingenuo, qualcosa di fondamentale per insegnare, addestrare ed evangelizzare.
TechZen,

26
Un'altra eccellente introduzione FRP: l'introduzione alla programmazione reattiva che mi è mancata dal mio collega André
Jonik,

5
Uno dei migliori che abbia mai visto, basato sull'esempio
Razmig

2
Trovo l'analogia del foglio di calcolo molto utile come prima impressione approssimativa (vedere la risposta di Bob: stackoverflow.com/a/1033066/1593924 ). Una cella di foglio di calcolo reagisce ai cambiamenti in altre celle (tira) ma non raggiunge e cambia gli altri (non spinge). Il risultato finale è che puoi cambiare una cella e molti altri "indipendentemente" aggiornano i loro display.
Jon Coombs,

Risposte:


931

Se vuoi avere un'idea di FRP, potresti iniziare con il vecchio tutorial di Fran del 1998, che ha illustrazioni animate. Per i documenti, inizia con Functional Reactive Animation e segui i link sul link delle pubblicazioni sulla mia home page e sul link FRP sul wiki di Haskell .

Personalmente, mi piace pensare a cosa significa FRP prima di affrontare come potrebbe essere implementato. (Il codice senza una specifica è una risposta senza una domanda e quindi "nemmeno sbagliato".) Quindi non descrivo il FRP in termini di rappresentazione / implementazione come Thomas K fa in un'altra risposta (grafici, nodi, spigoli, spari, esecuzione, eccetera). Ci sono molti stili di implementazione possibili, ma nessuna implementazione dice quello FRP è .

Mi associo alla semplice descrizione di Laurence G secondo cui FRP riguarda "tipi di dati che rappresentano un valore 'nel tempo'". La programmazione imperativa convenzionale cattura questi valori dinamici solo indirettamente, attraverso lo stato e le mutazioni. La storia completa (passato, presente, futuro) non ha una rappresentazione di prima classe. Inoltre, possono essere catturati (indirettamente) solo valori in evoluzione discreta , poiché il paradigma imperativo è temporalmente discreto. Al contrario, FRP acquisisce direttamente questi valori in evoluzione e non ha difficoltà con valori in continua evoluzione.

La FRP è anche insolita in quanto è concorrente senza scontrarsi con il nido teorico e pragmatico dei ratti che affligge la concorrenza imperativa. Semanticamente, la concorrenza di FRP è a grana fine , determinato , e continua . (Sto parlando di significato, non di implementazione. Un'implementazione può o meno implicare concorrenza o parallelismo.) La determinazione semantica è molto importante per il ragionamento, sia rigoroso che informale. Mentre la concorrenza aggiunge un'enorme complessità alla programmazione imperativa (a causa dell'interlacciamento non deterministico), è semplice in FRP.

Allora, cos'è l'FRP? Avresti potuto inventarlo da solo. Inizia con queste idee:

  • I valori dinamici / in evoluzione (ovvero i valori "nel tempo") sono valori di prima classe in se stessi. Puoi definirli e combinarli, passarli dentro e fuori dalle funzioni. Ho chiamato queste cose "comportamenti".

  • I comportamenti sono costituiti da alcuni primitivi, come comportamenti (statici) costanti e tempo (come un orologio), e quindi con una combinazione sequenziale e parallela. n comportamenti si combinano applicando una funzione n-ary (su valori statici), "punto-punto", cioè continuamente nel tempo.

  • Per tenere conto di fenomeni discreti, avere un altro tipo (famiglia) di "eventi", ognuno dei quali ha un flusso (finito o infinito) di occorrenze. Ogni ricorrenza ha un tempo e un valore associati.

  • Per inventare il vocabolario compositivo dal quale possono essere costruiti tutti i comportamenti e gli eventi, gioca con alcuni esempi. Continua a decostruire in pezzi più generali / semplici.

  • In modo che tu sappia che sei su un terreno solido, dai a tutto il modello una base compositiva, usando la tecnica della semantica denotazionale, il che significa solo che (a) ogni tipo ha un corrispondente tipo matematico semplice e preciso di "significati", e ( b) ogni primitivo e operatore ha un significato semplice e preciso in funzione dei significati dei componenti. Mai e poi mai mescolare considerazioni di implementazione nel processo di esplorazione. Se questa descrizione è per te incomprensibile, consulta (a) Design denotazionale con morfismi di classe di tipo , (b) Programmazione reattiva funzionale Push-pull (ignorando i bit di implementazione) e (c) la pagina dei wikibooks di Haskell della semantica denotazionale. Attenzione che la semantica denotazionale ha due parti, dai suoi due fondatori Christopher Strachey e Dana Scott: la parte Strachey più semplice e utile e la parte Scott più difficile e meno utile (per la progettazione di software).

Se ti attieni a questi principi, mi aspetto che otterrai qualcosa di più o di meno nello spirito di FRP.

Dove ho preso questi principi? Nella progettazione del software, faccio sempre la stessa domanda: "cosa significa?". La semantica denotazionale mi ha dato un quadro preciso per questa domanda, e una che si adatta alla mia estetica (a differenza della semantica operativa o assiomatica, che mi lascia entrambi insoddisfatto). Quindi mi sono chiesto che cos'è il comportamento? Presto mi sono reso conto che la natura temporalmente discreta del calcolo imperativo è una sistemazione per un particolare stile di macchina , piuttosto che una descrizione naturale del comportamento stesso. La descrizione più semplice e precisa del comportamento che mi viene in mente è semplicemente "funzione del tempo (continuo)", quindi questo è il mio modello. Deliziosamente, questo modello gestisce la concorrenza continua e deterministica con facilità e grazia.

È stata una vera sfida implementare questo modello in modo corretto ed efficiente, ma questa è un'altra storia.


78
Sono stato a conoscenza della programmazione reattiva funzionale. Sembra legato alla mia ricerca (nella grafica statistica interattiva) e sono sicuro che molte delle idee sarebbero utili per il mio lavoro. Tuttavia, trovo molto difficile superare la lingua - devo davvero imparare a conoscere la "semantica denotazionale" e i "morfismi della classe di tipo" per capire cosa sta succedendo? Un'introduzione generale dell'argomento all'argomento sarebbe molto utile.
Hadley,

212
@Conal: sai chiaramente di cosa stai parlando, ma la tua lingua presume che io abbia un dottorato in matematica computazionale, che io no. Ho un background nell'ingegneria dei sistemi e oltre 20 anni di esperienza con i computer e i linguaggi di programmazione, tuttavia sento che la tua risposta mi lascia perplesso. Ti sfido a ripubblicare la tua risposta in inglese ;-)
mindplay.dk,

50
@ minplay.dk: le tue osservazioni non mi danno molto da fare su ciò che in particolare non capisci, e non sono incline a fare ipotesi selvagge su quale particolare sottoinsieme di inglese stai cercando. Tuttavia, ti invito a dire specificamente su quali aspetti della mia spiegazione sopra stai inciampando, in modo che io e gli altri possiamo aiutarti. Ad esempio, ci sono parole particolari che desideri vengano definite o concetti per i quali desideri aggiungere riferimenti? Mi piace molto migliorare la chiarezza e l'accessibilità della mia scrittura, senza smorzarla.
Conal,

27
"Determinazione" / "determinata" significa che esiste un singolo valore corretto ben definito. Al contrario, quasi tutte le forme di concorrenza imperativa possono dare risposte diverse, a seconda di uno scheduler o se stai guardando o no, e possono anche bloccarsi. "Semantico" (e più specificamente "denotazionale") si riferisce al valore ("denotazione") di un'espressione o rappresentazione, in contrasto con "operativo" (come viene calcolata la risposta o quanto spazio e / o tempo viene consumato da ciò che tipo di macchina).
Conal,

18
Sono d'accordo con @ mindplay.dk anche se non posso vantarmi di essere stato sul campo per molto tempo. Anche se sembra che tu sappia di cosa stai parlando, non mi ha dato una comprensione rapida, breve e semplice di cosa si tratta, dato che sono abbastanza viziato da aspettarmi su SO. Questa risposta mi ha portato principalmente a moltissime nuove domande senza rispondere davvero al mio primo. Spero che condividere l'esperienza di essere ancora relativamente ignoranti in questo campo possa darti un'idea di quanto tu debba essere semplice e breve. Vengo da uno sfondo simile all'OP, a proposito.
Aske B.

739

Nella pura programmazione funzionale, non ci sono effetti collaterali. Per molti tipi di software (ad esempio qualsiasi cosa con l'interazione dell'utente) sono necessari effetti collaterali a un certo livello.

Un modo per ottenere un comportamento simile agli effetti collaterali pur mantenendo uno stile funzionale è utilizzare la programmazione reattiva funzionale. Questa è la combinazione di programmazione funzionale e programmazione reattiva. (L'articolo di Wikipedia a cui ti sei collegato riguarda quest'ultimo.)

L'idea alla base della programmazione reattiva è che ci sono alcuni tipi di dati che rappresentano un valore "nel tempo". I calcoli che coinvolgono questi valori che cambiano nel tempo avranno essi stessi valori che cambiano nel tempo.

Ad esempio, è possibile rappresentare le coordinate del mouse come una coppia di valori interi nel tempo. Diciamo che abbiamo avuto qualcosa di simile (questo è pseudo-codice):

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

In qualsiasi momento nel tempo, xey avrebbe le coordinate del mouse. A differenza della programmazione non reattiva, dobbiamo eseguire questa assegnazione una sola volta e le variabili xey rimarranno automaticamente "aggiornate". Questo è il motivo per cui la programmazione reattiva e la programmazione funzionale funzionano così bene insieme: la programmazione reattiva elimina la necessità di mutare le variabili pur consentendo di fare molto di ciò che si potrebbe ottenere con le mutazioni variabili.

Se poi eseguiamo alcuni calcoli basati su questo, anche i valori risultanti saranno valori che cambiano nel tempo. Per esempio:

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

In questo esempio, minXsarà sempre 16 in meno della coordinata x del puntatore del mouse. Con le librerie sensibili alla reattività potresti dire qualcosa del tipo:

rectangle(minX, minY, maxX, maxY)

E una casella 32x32 verrà disegnata attorno al puntatore del mouse e lo seguirà ovunque si muova.

Ecco un buon documento sulla programmazione reattiva funzionale .


25
Quindi la programmazione reattiva è una forma di programmazione dichiarativa?
troelskn,

31
> Quindi la programmazione reattiva è una forma di programmazione dichiarativa allora? La programmazione reattiva funzionale è una forma di programmazione funzionale, che è una forma di programmazione dichiarativa.
Conal,

7
@ user712092 Non proprio, no. Ad esempio, se chiamo sqrt(x)in C con la tua macro, ciò calcola sqrt(mouse_x())e mi restituisce un doppio. In un vero sistema reattivo funzionale, sqrt(x)restituirebbe un nuovo "doppio nel tempo". Se dovessi provare a simulare un sistema FR con #definete dovresti praticamente imprecare sulle variabili a favore delle macro. Inoltre, i sistemi FR in genere ricalcolano le cose solo quando devono essere ricalcolate, mentre l'uso delle macro significherebbe che rivaluteresti costantemente tutto, fino alle sottoespressioni.
Laurence Gonsalves

4
"Per molti tipi di software (ad esempio qualsiasi cosa con l'interazione dell'utente) sono necessari effetti collaterali a un certo livello." E forse solo a livello di implementazione. Ci sono molti effetti collaterali nell'implementazione della programmazione funzionale pura e pigra, e uno dei successi del paradigma è quello di mantenere molti di quegli effetti fuori dal modello di programmazione. Le mie incursioni nelle interfacce utente funzionali suggeriscono che possono anche essere programmate interamente senza effetti collaterali.
Conal,

4
@tieTYT x non viene mai riassegnato / modificato. Il valore di x è la sequenza di valori nel tempo. Un altro modo di vederlo è che invece di avere un valore "normale", come un numero, il valore di x è (concettualmente) una funzione che richiede tempo come parametro. (È un po 'una semplificazione eccessiva. Non puoi creare valori di tempo che ti permettano di predire il futuro di cose come la posizione del mouse.)
Laurence Gonsalves

144

Un modo semplice per arrivare a una prima intuizione su come è immaginare il tuo programma è un foglio di calcolo e tutte le variabili sono celle. Se una qualsiasi delle celle in un foglio di calcolo cambia, anche le celle che fanno riferimento a quella cella cambiano. È lo stesso con FRP. Immagina ora che alcune celle cambino da sole (o meglio, siano prese dal mondo esterno): in una situazione della GUI, la posizione del mouse sarebbe un buon esempio.

Ciò manca necessariamente piuttosto. La metafora si interrompe abbastanza rapidamente quando si utilizza effettivamente un sistema FRP. Per uno, di solito ci sono tentativi di modellare anche eventi discreti (ad esempio il mouse su cui si fa clic). Lo sto solo mettendo qui per darti un'idea di com'è.


3
Un esempio estremamente appropriato. È bello avere le cose teoriche, e forse alcune persone ne traggono le implicazioni senza ricorrere a un esempio di base, ma devo iniziare con quello che fa per me, non con quello astratto. Quello che ho ottenuto solo di recente (dai discorsi di Rx di Netflix!) È che RP (o Rx, comunque), rende questi "valori che cambiano" di prima classe e ti permette di ragionare su di essi, o di scrivere funzioni che fanno cose con loro. Scrivi funzioni per creare fogli di calcolo o celle, se lo desideri. E gestisce quando un valore termina (scompare) e ti consente di ripulire automaticamente.
Benjohn,

In questo esempio viene evidenziata la differenza tra la programmazione guidata dagli eventi e l'approccio reattivo, in cui si dichiarano semplicemente le dipendenze per utilizzare il routing intelligente.
kinjelom,

131

Per me si tratta di 2 significati diversi di simbolo =:

  1. In matematica x = sin(t)significa che xè un nome diverso per sin(t). Quindi scrivere x + yè la stessa cosa di sin(t) + y. La programmazione reattiva funzionale è come la matematica in questo senso: se scrivi x + y, viene calcolata con qualunque sia il valore di tal momento in cui viene usata.
  2. Nei linguaggi di programmazione simil-C (linguaggi imperativi), x = sin(t)è un compito: significa che xmemorizza il valore di sin(t) preso al momento del compito.

5
Buona spiegazione Penso che potresti anche aggiungere che "tempo" nel senso di FRP è normalmente "qualsiasi cambiamento dall'input esterno". Ogni volta che una forza esterna modifica un input di FRP, hai spostato il "tempo" in avanti e ricalcolato di nuovo tutto ciò che è interessato dalla modifica.
Didier A.

4
In matematica x = sin(t)significa xè il valore di sin(t)per il dato t. E ' non è un nome diverso per sin(t)come la funzione. Altrimenti sarebbe x(t) = sin(t).
Dmitri Zaitsev,

+ Il segno di Dmitri Zaitsev Equals ha diversi significati in matematica. Uno di questi è che ogni volta che vedi il lato sinistro puoi scambiarlo con il lato destro. Ad esempio 2 + 3 = 5o a**2 + b**2 = c**2.
user712092

71

OK, dalle conoscenze di base e dalla lettura della pagina di Wikipedia a cui hai indicato, sembra che la programmazione reattiva sia qualcosa di simile al calcolo del flusso di dati ma con specifici "stimoli" esterni che innescano un insieme di nodi per attivare ed eseguire i loro calcoli.

Questo è abbastanza adatto alla progettazione dell'interfaccia utente, ad esempio, in cui toccando un controllo dell'interfaccia utente (ad esempio, il controllo del volume su un'applicazione di riproduzione musicale) potrebbe essere necessario aggiornare vari elementi del display e il volume effettivo dell'uscita audio. Quando modifichi il volume (uno slider, diciamo) che corrisponderebbe alla modifica del valore associato a un nodo in un grafico diretto.

Vari nodi che hanno bordi da quel nodo "valore del volume" verrebbero automaticamente attivati ​​e tutti i calcoli e gli aggiornamenti necessari si riverserebbero naturalmente nell'applicazione. L'applicazione "reagisce" allo stimolo dell'utente. La programmazione reattiva funzionale sarebbe solo l'implementazione di questa idea in un linguaggio funzionale, o generalmente all'interno di un paradigma di programmazione funzionale.

Per ulteriori informazioni sul "calcolo del flusso di dati", cerca queste due parole su Wikipedia o utilizzando il tuo motore di ricerca preferito. L'idea generale è questa: il programma è un grafico diretto di nodi, ognuno dei quali esegue un semplice calcolo. Questi nodi sono collegati tra loro tramite collegamenti grafici che forniscono gli output di alcuni nodi agli input di altri.

Quando un nodo si attiva o esegue il suo calcolo, i nodi collegati alle sue uscite hanno i loro ingressi corrispondenti "innescati" o "contrassegnati". Qualsiasi nodo con tutti gli input attivati ​​/ contrassegnati / disponibili si attiva automaticamente. Il grafico potrebbe essere implicito o esplicito a seconda di come viene implementata la programmazione reattiva.

I nodi possono essere visti come spari in parallelo, ma spesso vengono eseguiti in serie o con parallelismo limitato (ad esempio, potrebbero esserci alcuni thread che li eseguono). Un esempio famoso è stato il Manchester Dataflow Machine , che (IIRC) ha utilizzato un'architettura di dati con tag per pianificare l'esecuzione dei nodi nel grafico attraverso una o più unità di esecuzione. Il calcolo del flusso di dati si adatta abbastanza bene alle situazioni in cui l'attivazione dei calcoli in modo asincrono che dà origine a cascate di calcoli funziona meglio del tentativo di far eseguire l'esecuzione da un clock (o clock).

La programmazione reattiva importa questa idea di "cascata di esecuzione" e sembra pensare al programma in modo simile al flusso di dati, ma con la condizione che alcuni dei nodi siano agganciati al "mondo esterno" e che le cascate di esecuzione siano innescate quando questi sensori -come i nodi cambiano. L'esecuzione del programma sembrerebbe quindi qualcosa di analogo a un arco riflesso complesso. Il programma può o meno essere sostanzialmente sessile tra gli stimoli o può stabilirsi in uno stato sostanzialmente sessile tra gli stimoli.

La programmazione "non reattiva" sarebbe la programmazione con una visione molto diversa del flusso di esecuzione e della relazione con gli input esterni. È probabile che sia in qualche modo soggettivo, poiché le persone saranno probabilmente tentate di dire che qualsiasi cosa che risponda a input esterni "reagisce" a loro. Ma guardando lo spirito della cosa, un programma che esegue il polling di una coda di eventi a un intervallo fisso e invia tutti gli eventi trovati su funzioni (o thread) è meno reattivo (perché si occupa solo dell'input dell'utente a un intervallo fisso). Ancora una volta, è lo spirito della cosa qui: si può immaginare di mettere un'implementazione di polling con un intervallo di polling rapido in un sistema a un livello molto basso e programmare su di esso in modo reattivo.


1
OK, ci sono alcune buone risposte sopra adesso. Devo rimuovere il mio post? Se vedo due o tre persone che dicono che non aggiunge nulla, lo eliminerò a meno che il suo conteggio utile non aumenti. Inutile lasciarlo qui a meno che non aggiunga qualcosa di valore.
Thomas Kammeyer,

3
hai menzionato il flusso di dati, in modo da aggiungere un valore IMHO.
Rainer Joswig,

Questo è ciò che QML dovrebbe essere, a quanto pare;)
mlvljr

3
Per me, questa risposta è stata la più facile da capire, soprattutto perché l'uso di analoghi naturali come "ripple attraverso l'applicazione" e "nodi sensoriali". Grande!
Akseli Palén,

1
sfortunatamente, il collegamento al Manchester Dataflow Machine è morto.
Pac0

65

Dopo aver letto molte pagine su FRP, alla fine mi sono imbattuto in questo scritto illuminante su FRP, che mi ha fatto finalmente capire di cosa si tratta veramente.

Cito di seguito Heinrich Apfelmus (autore di banane reattive).

Qual è l'essenza della programmazione reattiva funzionale?

Una risposta comune sarebbe che "FRP si basa sulla descrizione di un sistema in termini di funzioni che variano nel tempo anziché in uno stato mutabile", e ciò non sarebbe certamente sbagliato. Questo è il punto di vista semantico. Ma a mio avviso, la risposta più profonda e soddisfacente è data dal seguente criterio puramente sintattico:

L'essenza della programmazione reattiva funzionale è di specificare completamente il comportamento dinamico di un valore al momento della dichiarazione.

Ad esempio, prendi l'esempio di un contatore: hai due pulsanti etichettati “Su” e “Giù” che possono essere usati per aumentare o diminuire il contatore. Imperativamente, dovresti prima specificare un valore iniziale e poi cambiarlo ogni volta che viene premuto un pulsante; qualcosa come questo:

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

Il punto è che al momento della dichiarazione viene specificato solo il valore iniziale per il contatore; il comportamento dinamico del contatore è implicito nel resto del testo del programma. Al contrario, la programmazione reattiva funzionale specifica l'intero comportamento dinamico al momento della dichiarazione, in questo modo:

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

Ogni volta che vuoi capire le dinamiche del contatore, devi solo guardare la sua definizione. Tutto ciò che può accadere apparirà sul lato destro. Ciò è in netto contrasto con l'approccio imperativo in cui dichiarazioni successive possono modificare il comportamento dinamico di valori precedentemente dichiarati.

Quindi, nella mia comprensione, un programma FRP è un insieme di equazioni: inserisci qui la descrizione dell'immagine

j è discreto: 1,2,3,4 ...

fdipende da tciò, questo incorpora la possibilità di modellare stimoli esterni

tutto lo stato del programma è incapsulato in variabili x_i

La libreria FRP si occupa di far progredire il tempo, in altre parole, di prendere jaj+1 .

Spiego queste equazioni in modo molto più dettagliato in questo video.

MODIFICARE:

Circa 2 anni dopo la risposta originale, recentemente sono giunto alla conclusione che le implementazioni FRP hanno un altro aspetto importante. Devono (e di solito lo fanno) risolvere un importante problema pratico: invalidare la cache .

Le equazioni per x_i-s descrivono un grafico delle dipendenze. Quando alcune delle x_imodifiche alla volta jnon è necessario aggiornare tutti gli altri x_i'valori j+1, non è necessario ricalcolare tutte le dipendenze perché alcune x_i'potrebbero essere indipendenti x_i.

Inoltre, x_i-s che cambiano può essere aggiornato in modo incrementale. Per esempio prendiamo in considerazione un'operazione di carta f=g.map(_+1)a Scala, dove fe gsono Listdi Ints. Qui fcorrisponde x_i(t_j)ed gè x_j(t_j). Ora se antepongo un elemento a gallora sarebbe inutile eseguire l' mapoperazione per tutti gli elementi in g. Alcune implementazioni di FRP (ad esempio reflex-frp ) mirano a risolvere questo problema. Questo problema è noto anche come calcolo incrementale.

In altre parole, i comportamenti (i x_i-s) in FRP possono essere pensati come calcoli memorizzati nella cache. È compito del motore FRP invalidare e ricalcolare in modo efficiente queste cache (le x_i-s) se alcune delle f_i-s cambiano.


4
Sono stato lì con te fino a quando non hai seguito equazioni discrete . L'idea alla base di FRP era il tempo continuo , dove non c'è " j+1". Invece, pensa alle funzioni del tempo continuo. Come ci hanno mostrato Newton, Leibniz e altri, spesso è profondamente utile (e "naturale" in senso letterale) descrivere queste funzioni in modo differenziato, ma continuamente, usando integrali e sistemi di ODE. Altrimenti, stai descrivendo un algoritmo di approssimazione (e uno scadente) invece della cosa stessa.
Conal,

Il linguaggio layx del modello HTML e dei vincoli di layout sembra esprimere elementi di FRP.

@Conal questo mi fa chiedere in che modo FRP è diverso dagli ODE. In che modo differiscono?
jhegedus,

@jhegedus In tale integrazione (possibilmente ricorsiva, cioè ODE) fornisce uno dei mattoni di FRP, non del tutto. Ogni elemento del vocabolario FRP (incluso ma non limitato all'integrazione) è spiegato con precisione in termini di tempo continuo. Questa spiegazione aiuta?
Conal,


29

Disclaimer: la mia risposta è nel contesto di rx.js - una libreria di "programmazione reattiva" per Javascript.

Nella programmazione funzionale, anziché scorrere ogni elemento di una raccolta, si applicano funzioni di ordine superiore (HoF) alla raccolta stessa. Quindi l'idea alla base di FRP è che invece di elaborare ogni singolo evento, creare un flusso di eventi (implementato con un osservabile *) e applicare invece HoF a quello. In questo modo è possibile visualizzare il sistema come pipeline di dati che collegano gli editori agli abbonati.

I principali vantaggi dell'utilizzo di un osservabile sono:
i) estrae lo stato dal codice, ad es. Se si desidera che il gestore eventi venga licenziato solo per ogni 'n' evento o smetta di sparare dopo i primi 'n' eventi, o inizia a sparare solo dopo i primi 'n' eventi, puoi semplicemente usare gli HoF (filter, takeUntil, skip rispettivamente) invece di impostare, aggiornare e controllare i contatori.
ii) migliora la localizzazione del codice - se hai 5 gestori di eventi diversi che cambiano lo stato di un componente, puoi unire i loro osservabili e definire un singolo gestore di eventi sull'osservabile unito invece, combinando efficacemente 5 gestori di eventi in 1. Questo lo rende molto facile ragionare su quali eventi nell'intero sistema possono influenzare un componente, poiché è tutto presente in un singolo gestore.

  • Un osservabile è il doppio di un Iterabile.

Un Iterable è una sequenza consumata pigramente: ogni elemento viene estratto dall'iteratore ogni volta che lo desidera, e quindi l'enumerazione è guidata dal consumatore.

Un osservabile è una sequenza prodotta pigramente - ogni articolo viene spinto all'osservatore ogni volta che viene aggiunto alla sequenza, e quindi l'enumerazione è guidata dal produttore.


1
Grazie mille per questa semplice definizione di osservabile e la sua differenziazione da iterabili. Penso che spesso sia molto utile confrontare un concetto complesso con il suo ben noto doppio concetto per ottenere una vera comprensione.

2
"Quindi l'idea alla base di FRP è che invece di elaborare ogni singolo evento, creare un flusso di eventi (implementato con un osservabile *) e applicare invece HoF a quello." Potrei sbagliarmi, ma credo che questo non sia in realtà FRP ma piuttosto una bella astrazione sul modello di progettazione Observer che consente operazioni funzionali tramite HoF (il che è fantastico!) Mentre è ancora inteso per essere utilizzato con codice imperativo. Discussione sull'argomento - lambda-the-ultimate.org/node/4982
nqe

18

Amico, questa è un'idea strabiliante! Perché non l'ho scoperto nel 1998? Comunque, ecco la mia interpretazione del tutorial di Fran . I suggerimenti sono i benvenuti, sto pensando di avviare un motore di gioco basato su questo.

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()

In breve: se ogni componente può essere trattato come un numero, l'intero sistema può essere trattato come un'equazione matematica, giusto?


1
È un po 'tardi, ma comunque ... Frag è un gioco che utilizza FRP .
Arx,

14

Il libro di Paul Hudak, The Haskell School of Expression , non è solo una bella introduzione a Haskell, ma trascorre anche un bel po 'di tempo su FRP. Se sei un principiante con FRP, lo consiglio vivamente di darti un'idea di come funziona FRP.

C'è anche quella che sembra una nuova riscrittura di questo libro (pubblicato nel 2011, aggiornato nel 2014), The Haskell School of Music .


10

Secondo le risposte precedenti, sembra che matematicamente, pensiamo semplicemente in un ordine superiore. Invece di pensare a un valore x con tipo X , pensiamo a una funzione x : TX , dove T è il tipo di tempo, siano i numeri naturali, i numeri interi o il continuum. Ora quando scriviamo y : = x + 1 nel linguaggio di programmazione, intendiamo effettivamente l'equazione y ( t ) = x ( t ) + 1.


9

Funziona come un foglio di calcolo come indicato. Generalmente basato su un framework guidato da eventi.

Come per tutti i "paradigmi", la sua novità è discutibile.

Dalla mia esperienza di reti di attori a flusso distribuito, può facilmente cadere in preda a un problema generale di coerenza statale attraverso la rete di nodi, vale a dire che si finisce con molta oscillazione e intrappolamento in strani circuiti.

Questo è difficile da evitare poiché alcune semantiche implicano cicli o trasmissioni referenziali e possono essere piuttosto caotiche poiché la rete di attori converge (o no) su uno stato imprevedibile.

Allo stesso modo, alcuni stati potrebbero non essere raggiunti, nonostante abbiano bordi ben definiti, perché lo stato globale si allontana dalla soluzione. 2 + 2 possono o meno essere 4 a seconda di quando i 2 sono diventati 2 e se sono rimasti in quel modo. I fogli di calcolo dispongono di orologi sincroni e rilevamento di loop. Gli attori distribuiti generalmente no.

Tutto molto divertente :).



7

Questo articolo di Andre Staltz è la spiegazione migliore e più chiara che abbia mai visto finora.

Alcune citazioni dall'articolo:

La programmazione reattiva sta programmando con flussi di dati asincroni.

Inoltre, ti viene data una straordinaria serie di funzioni per combinare, creare e filtrare uno di questi flussi.

Ecco un esempio dei fantastici diagrammi che fanno parte dell'articolo:

Fai clic sul diagramma del flusso di eventi


5

Riguarda le trasformazioni matematiche dei dati nel tempo (o ignorando il tempo).

Nel codice questo significa purezza funzionale e programmazione dichiarativa.

I bug di stato sono un grosso problema nel paradigma imperativo standard. Vari bit di codice possono cambiare alcuni stati condivisi in diversi "tempi" nell'esecuzione dei programmi. Questo è difficile da affrontare.

In FRP descrivi (come nella programmazione dichiarativa) come i dati si trasformano da uno stato all'altro e cosa li innesca. Questo ti permette di ignorare il tempo perché la tua funzione sta semplicemente reagendo ai suoi input e usando i loro valori attuali per crearne uno nuovo. Ciò significa che lo stato è contenuto nel grafico (o albero) dei nodi di trasformazione ed è funzionalmente puro.

Ciò riduce notevolmente la complessità e i tempi di debug.

Pensa alla differenza tra A = B + C in matematica e A = B + C in un programma. In matematica stai descrivendo una relazione che non cambierà mai. In un programma, si dice che "In questo momento" A è B + C. Ma il prossimo comando potrebbe essere B ++, nel qual caso A non è uguale a B + C. Nella programmazione matematica o dichiarativa A sarà sempre uguale a B + C, indipendentemente dal momento in cui lo chiedi.

Quindi rimuovendo le complessità dello stato condiviso e modificando i valori nel tempo. Il programma è molto più facile da ragionare.

Un EventStream è un EventStream + alcune funzioni di trasformazione.

Un comportamento è un EventStream + Qualche valore in memoria.

Quando viene generato l'evento, il valore viene aggiornato eseguendo la funzione di trasformazione. Il valore che questo produce viene memorizzato nella memoria del comportamento.

I comportamenti possono essere composti per produrre nuovi comportamenti che sono una trasformazione su N altri comportamenti. Questo valore composto verrà ricalcolato quando gli eventi di input (comportamenti) vengono attivati.

"Poiché gli osservatori sono apolidi, ne abbiamo spesso bisogno per simulare una macchina a stati come nell'esempio di trascinamento. Dobbiamo salvare lo stato in cui è accessibile a tutti gli osservatori coinvolti, come nel percorso variabile sopra."

Citazione da - Deprecating The Observer Pattern http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf


Questo è esattamente quello che provo per la programmazione dichiarativa e tu descrivi l'idea meglio di me.
Neevek,

2

La breve e chiara spiegazione sulla programmazione reattiva appare su Cyclejs - Programmazione reattiva , utilizza campioni semplici e visivi.

Un [modulo / componente / oggetto] è reattivo significa che è pienamente responsabile della gestione del proprio stato reagendo a eventi esterni.

Qual è il vantaggio di questo approccio? È Inversion of Control , principalmente perché [module / Component / object] è responsabile di se stesso, migliorando l'incapsulamento usando metodi privati ​​rispetto a quelli pubblici.

È un buon punto di partenza, non una fonte completa di conoscenza. Da lì puoi saltare a documenti più complessi e profondi.


0

Dai un'occhiata a Rx, Extctive Reactive per .NET. Sottolineano che con IEnumerable stai praticamente "estraendo" da un flusso. Le query Linq su IQueryable / IEnumerable sono operazioni di set che "aspirano" i risultati da un set. Ma con gli stessi operatori su IObservable puoi scrivere query Linq che "reagiscono".

Ad esempio, è possibile scrivere una query Linq come (da m in MyObservableSetOfMouseMovements in cui mX <100 e mY <100 selezionano un nuovo punto (mX, mY)).

e con le estensioni Rx, il gioco è fatto: hai un codice UI che reagisce al flusso in entrata dei movimenti del mouse e disegna ogni volta che sei nella casella 100,100 ...


0

FRP è una combinazione di programmazione funzionale (paradigma di programmazione basato sull'idea di tutto è una funzione) e paradigma di programmazione reattiva (basato sull'idea che tutto è un flusso (osservatore e filosofia osservabile)). Dovrebbe essere il migliore dei mondi.

Dai un'occhiata al post di Andre Staltz sulla programmazione reattiva per iniziare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.