Indice
- Definizione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
- Caratteristiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
- Propagazione del cambiamento . . . . . . . . . . . . . . . . . . . . 3
- Strategie di propagazione . . . . . . . . . . . . . . . . . . . . . . . . . 7
- Modelli di valutazione . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
- Glitch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
- Metodi di rimozione . . . . . . . . . . . . . . . . . . . . . 12
- Flussi asincroni di dati ed eventi . . . . . . . . . . . . . . . . . . . . 15
- Entità osservabili e osservatrici . . . . . . . . . . . . . . . . . . . . . . 17
- Entità osservabile . . . . . . . . . . . . . . . . . . . . . . . . 17
- Entità osservatrice . . . . . . . . . . . . . . . . . . . . . . . . 18
- Contesti d’utilizzo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
- Vantaggi rispetto ad altri approcci . . . . . . . . . . . . . . . . . . . 19
- Programmazione reattiva distribuita . . . . . . . . . . . . . . . . . . 21
- Programmazione reattiva funzionale . . . . . . . . . . . . . . . . . . . 22
- Functional Programming . . . . . . . . . . . . . . . . . . . 22
- Functional Reactive Programming . . . . . . . . . . . . . . 23
- Astrazioni di base . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
- Tassonomia dei linguaggi reattivi . . . . . . . . . . . . . . . . . . . . 25
- Uso industriale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
- Il manifesto reattivo . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
- Bibliografia 32
Programmazione reattiva
Questi appunti si propongono di descrivere il panorama della programmazione reattiva partendo dal principio, cioè dalle sue caratteristiche fondanti, per poi procedere a piccoli passi verso argomenti più di dettaglio cercando sempre, prima dell’introduzione di ognuno di essi (per quanto possibile), di fornire al lettore le nozioni di base per poter affrontare la lettura senza difficoltà.
Definizione
Premettendo che non è tuttora esistente una definizione ufficialmente riconosciuta di programmazione reattiva (RP, Reactive Programming), una di quelle che condensa al meglio la filosofia di questo paradigma è probabilmente la seguente: la programmazione reattiva è un paradigma di programmazione che si avvale dei concetti del pattern Observer per gestire ed elaborare dati ed eventi tramite flussi asincroni e osservabili dall’esterno, in accordo con la nozione di propagazione del cambiamento.
Caratteristiche
Dalla definizione si può evincere come la programmazione reattiva sia un paradigma fortemente correlato all’utilizzo di stream asincroni di dati/eventi e perciò particolarmente utile nella programmazione event-driven, efficace per lo sviluppo di applicazioni che necessitano di risposte reattive a stimoli provenienti in maniera asincrona da molteplici fonti, situazione riscontrabile tipicamente su piattaforme web e mobile. Questo approccio prevalentemente asincrono indebolisce i legami (loose coupling) tra i componenti dei programmi, producendo codice altamente modulare e dunque più facilmente modificabile, manutenibile ed estensibile.
È importante precisare che si parla di flussi di informazioni osservabili, i cui dati ed eventi emessi sono perciò catturabili dall’esterno e gestibili all’occorrenza da tutti coloro che necessitino di continui aggiornamenti riguardo uno specifico evento d’interesse. All’interno di questo contesto trova dunque facilmente posto il Design Pattern Observer [11] della GoF, il quale definisce una o più dipendenze fra oggetti, così che quando uno di essi cambia stato, tutti i suoi sottoscrittori vengono notificati automaticamente e possono reagire di conseguenza.
Per quanto riguarda invece il concetto di propagazione del cambiamento (Propagation of change), si tratta di una nozione presente in letteratura e su cui si fonda il pensiero della programmazione reattiva; verrà esposto in maniera più dettagliata nella prossima sottosezione.
1 Gang of Four, così vengono spesso chiamati Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, autori del libro Design Patterns: Elements of Reusable Object-Oriented Software.
Propagazione del cambiamento
Nel paradigma di programmazione reattiva tutto può essere trasmesso tramite stream asincroni: dati, eventi, messaggi, chiamate, situazioni di successo e di errore. Si possono osservare questi flussi e reagire quando da essi vengono emessi valori. Questo ha un effetto interessante sulle applicazioni poiché tende a farle diventare intrinsecamente asincrone ed event-driven, reattive nel gestire i cambiamenti in atto e le loro propagazioni spesso prive di sincronia. All’interno del contesto delineatosi si afferma l’idea di propagazione del cambiamento, concetto che sta alla base del paradigma in esame.
Si prenda come esempio esplicativo la seguente istruzione in pseudo-codice:
a := b + c
Se si utilizza un linguaggio imperativo, l’assegnamento di cui sopra applica alla variabile a il risultato dovuto alla somma tra le variabili b e c e questo avviene nel preciso istante nel quale l’espressione viene valutata. Ciò significa che un cambiamento successivo dei valori di b e/o di c non produce alcun effetto sulla variabile a, la quale mantiene sempre il valore originariamente calcolato (a meno, ovviamente, di una nuova valutazione dell’espressione stessa). In programmazione reattiva, invece, il concetto di propagazione del cambiamento rende il procedimento differente: il valore di a viene infatti sempre aggiornato automaticamente ogniqualvolta i valori delle variabili b e/o c cambiano, senza prevedere la necessità da parte del programma di rieseguire l’istruzione di assegnamento di a.
La logica della propagazione del cambiamento può essere per esempio implementata per mezzo di un grafo i cui nodi rappresentano i valori da tenere aggiornati e i cui archi rappresentano le relazioni di dipendenza che coinvolgono i nodi. Tramite questo tipo di architettura si viene quindi a creare una rete di dipendenze computazionali ed il cambiamento di un nodo (cioè di un valore) scatena una reazione che implica l’attraversamento degli eventuali archi ad esso collegati e il conseguente aggiornamento di tutti i nodi raggiunti.
Per rendere più chiari questi concetti, viene esposto di seguito un semplice esempio in pseudo-codice.
number1 := 4
number2 := 6
sum := number1 + number2
average := sum / 2
Il codice mostra l’assegnamento dei valori 4 e 6 a due variabili (number1 e number2), con successivo calcolo e salvataggio della somma e della media aritmetica (rispettivamente all’interno delle variabili sum ed average). In questo caso risulta estremamente semplice costruire il corrispondente grafo delle dipendenze per la propagazione dei cambiamenti, in figura 1:
Figura 1: Grafo delle dipendenze, somma e media di due numeri
Si può facilmente notare come il primo nodo genitore (average) abbia solo un nodo figlio (sum), perciò dipenda solo da esso, mentre quest’ultimo dipenda invece da due nodi figlio (rappresentati dalle variabili number1 e number2); questi ultimi sono attualmente anche nodi periferici del grafo orientato, perciò si possono considerare ‘autonomi’ perché non dipendono da nessun altro nodo figlio e nell’attuale configurazione non possono quindi mai subire modifiche propagate.
Seguendo l’approccio imperativo, i valori delle variabili sum ed average vengono assegnati durante l’esecuzione del frammento di codice e non risultano aggiornati dopo una modifica dei valori delle variabili che rappresentano i due numeri. Al contrario, seguendo l’approccio dettato dalla programmazione reattiva e in particolare dal concetto di propagazione del cambiamento su cui il paradigma si basa, ogni modifica dei valori delle variabili number1 e number2 propaga il cambiamento al nodo genitore sum, il quale aggiorna la somma e propaga a sua volta l’informazione di cambiamento al nodo genitore average, il quale infine aggiorna il valore della media aritmetica.
La situazione cambia leggermente nel prossimo esempio, sempre in pseudo-codice.
numbers := [1, 2, 3, 4, 5, 6]
sum := 0
for each number in numbers
sum := sum + number
end
average := sum / count(numbers)
In questo caso non sono presenti solo due numeri di cui far la somma e la media, bensì un array di numeri grande a piacere, perciò è necessario iterare l’intero vettore per calcolare la somma totale dei suoi elementi e ricavarne la lunghezza (count(numbers)) per quantificare il valore al denominatore nella formula per la media aritmetica. In figura 2 viene mostrato il grafo delle dipendenze per la propagazione dei cambiamenti relativo a questo esempio:
Figura 2: Grafo delle dipendenze, somma e media di un array di numeri
Si può osservare come il nodo genitore average, contrariamente all’esempio precedente, si ritrovi ora ad avere due nodi figlio da cui dipende, sum e numbers, e come quest’ultimo abbia due nodi genitore (average e sum). Questo accade per il semplice motivo che la variabile contenente la media viene assegnata tramite un’espressione che coinvolge entrambe le variabili (average := sum / count(numbers)); average si trova a dipendere quindi non più solo dal valore della somma ma anche dalla lunghezza dell’array di numeri. Se numbers subisce una mutazione, la notifica del cambiamento viene propagata attraverso il grafo delle dipendenze a sum ed average, ma solo il primo può essere aggiornato poiché il secondo non possiede ancora tutte la nuove informazioni per poter calcolare la media; una volta che anche il nodo sum notifica il cambiamento del valore della somma al nodo genitore, allora questo viene aggiornato.
2 Nel caso in cui average calcolasse comunque la media senza possedere tutte le informazioni aggiornate si potrebbe verificare un malfunzionamento dovuto ad inconsistenza dei dati chiamato glitch, argomento della sottosezione 1.2.4.
Strategie di propagazione
Le principali strategie adottabili per la propagazione del cambiamento nella programmazione reattiva sono le seguenti:
- Complete propagation
Ogni nodo, durante la propagazione, invia il suo attuale stato completo ai nodi dipendenti; in questo caso, tutto lo stato precedente del nodo viene perso durante l’aggiornamento al nuovo stato. Questa strategia presenta un alto carico computazionale, in quanto ogni nodo mutato propaga il suo completo stato, perciò non è adatto nei casi in cui il grafo delle dipendenze computazionali sia particolarmente complesso o nel caso in cui gli stati necessitino di grandi quantità di dati per essere descritti. - Delta propagation
Nel momento della propagazione i nodi interessati inviano solo una porzione (un delta) del loro stato ai nodi dipendenti, cioè solo le frazioni di informazione che sono state effettivamente coinvolte da modifiche. Le porzioni di stato precedenti non soggette ad alterazioni permangono all’interno dei nodi, non vengono perse. Questo tipo di approccio tende a minimizzare la quantità di dati da trasportare lungo la catena di dipendenze durante la propagazione dei cambiamenti, dunque risulta efficiente anche nei casi in cui si presenti un grafo delle dipendenze complesso o una gran quantità di dati per la rappresentazione degli stati, situazioni nelle quali la complete propagation mostra invece limitazioni. - Batch propagation
La batch propagation è un approccio che prevede la non immediata propagazione dei cambiamenti, cioè la propagazione ritardata nel tempo. È un metodo di ottimizzazione: se due o più cambiamenti vicini nel tempo si annullano a vicenda (fanno cioè sì che si ritorni allo stato di partenza, precedente ai cambiamenti stessi), oppure un aggiornamento non modifica alcuno stato, la propagazione non avviene, in quanto non è in atto nessun cambiamento complessivo nel concreto. Al contrario, nel caso due o più cambiamenti vicini nel tempo generino un’effettiva mutazione di stato nel grafo, viene propagato solo l’ultimo cambiamento, infatti i precedenti risultano ormai obsoleti. Questa strategia minimizza il numero di propagazioni e quindi di percorrenza delle dipendenze nel grafo e di carico computazionale complessivo, ma tende a rendere il sistema meno reattivo ai cambiamenti, poiché essi vengono ritardati nel tempo. È dunque cruciale adottare un tempo di ritardo corretto, al fine di ritardare il cambiamento riducendo il carico di elaborazione ma al tempo stesso non diminuire eccessivamente la reattività del sistema; si delinea quindi una strategia efficace nel caso in cui le computazioni e il trasferimento di dati tra i nodi si rivelino particolarmente onerosi e sia tollerabile un leggero ritardo di reattività ai cambiamenti. - Invalidity notification propagation
Non si delinea propriamente come una strategia implementativa, quanto più come una ”sotto-strategia”, cioè un approccio utilizzabile potenzialmente accanto ad ognuna delle tre strategie precedentemente descritte. È utilizzabile inoltre soltanto nei casi in cui il sistema di interesse possieda un grafo delle dipendenze con direzionalità di propagazione di tipo pull oppure hybrid (verranno descritti nella prossima sottosezione). La invalidity notification propagation consiste nella richiesta di aggiornamento (pull update) da parte di un nodo che riscontra di possedere uno stato non valido, oppure che riceve un messaggio di propagazione non valido. In questo caso il nodo interessato scarta l’anomalia e richiede ai nodi da cui dipende un nuovo aggiornamento in merito. Dopo la ricezione del nuovo aggiornamento, e solo nel caso questo risulti valido, il nodo si aggiorna e lo propaga come di consueto attraverso le dipendenze ad esso collegate.
Si ricorda che è possibile anche progettare architetture reattive che contemplino la contemporanea presenza di più strategie, fra loro non in contrasto, al fine di ricavare vantaggi da tutte in base alle differenti situazioni. Un’ottima idea può essere per esempio quella di impiegare la delta propagation insieme alla batch propagation.
Modelli di valutazione
Per modello di valutazione (evaluation model) nella Reactive Programming si intende la dinamica direzionale adottata per la propagazione dei cambiamenti nel grafo delle dipendenze computazionali del sistema reattivo. Il modello viene applicato a livello di linguaggio e rimane trasparente al programmatore, che non deve quindi preoccuparsi di propagare i dati manualmente.
Come già specificato precedentemente, un nodo trasmette il cambiamento ad altri nodi suoi dipendenti seguendo la rete di relazioni rappresentata dal grafo; il nodo che invia può essere quindi visto come un produttore di dati (informazioni di cambiamento) e i nodi che ricevono come dei consumatori di dati, in accordo con la logica produttore-consumatore evidenziata in figura 3.
A questo punto, però, bisogna prendere un’importante decisione di progettazione: è necessario infatti scegliere quale modello di valutazione adottare, ognuno di essi offre vantaggi in alcune situazioni e svantaggi in altre. La letteratura presenta due principali modelli [1][9] a cui si aggiunge un terzo ibrido fra i primi due:
- Push-based
Nel modello push-based la reazione ha inizio quando un nodo produttore (origine) cambia stato aggiornandosi, in questo caso ”spinge” (push) l’informazione di cambiamento attraverso il grafo verso i nodi consumatori suoi dipendenti (destin
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.