Programmazione funzionale - Immutabilità


12

Sto cercando di capire come gestire i dati immutabili in FP (in particolare in F #, ma anche altri FP sono ok) e rompere la vecchia abitudine del pensiero pieno di stato (stile OOP). Una parte della risposta selezionata alla domanda qui ha ribadito la mia ricerca di eventuali riscritti relativi a problemi risolti da rappresentazioni statali in OOP con immutabili in FP (Ad esempio: una coda con produttori e consumatori). Qualche idea o link sono i benvenuti? Grazie in anticipo.

Modifica : per chiarire un po 'di più la domanda, in che modo le strutture immutabili (es: coda) sarebbero condivise contemporaneamente su più thread (es: produttore e consumatore) in FP


Un modo per gestire i problemi di concorrenza è quello di fare copie della coda ogni volta (un po 'costoso, ma funziona).
Giobbe

infoq.com/presentations/Functional-Data-Structures-in-Scala Potresti trovare questo discorso perspicace.
deadalnix,

Risposte:


19

Sebbene talvolta sia espresso in questo modo, la programmazione funzionale¹ non impedisce calcoli con stato. Ciò che fa è forzare il programmatore a rendere esplicito lo stato.

Ad esempio, prendiamo la struttura di base di alcuni programmi usando una coda imperativa (in alcuni pseudolingua):

q := Queue.new();
while (true) {
    if (Queue.is_empty(q)) {
        Queue.add(q, producer());
    } else {
        consumer(Queue.take(q));
    }
}

La struttura corrispondente con una struttura di dati di coda funzionale (ancora in un linguaggio imperativo, in modo da affrontare una differenza alla volta) sarebbe simile a questa:

q := Queue.empty;
while (true) {
    if (q = Queue.empty) {
        q := Queue.add(q, producer());
    } else {
        (tail, element) := Queue.take(q);
        consumer(element);
        q := tail;
    }
}

Poiché la coda è ora immutabile, l'oggetto stesso non cambia. In questo pseudo-codice, qessa stessa è una variabile; le assegnazioni q := Queue.add(…)e q := tailfarle puntare a un oggetto diverso. L'interfaccia delle funzioni della coda è cambiata: ognuna deve restituire il nuovo oggetto coda che risulta dall'operazione.

In un linguaggio puramente funzionale, cioè in un linguaggio senza effetti collaterali, è necessario rendere esplicito tutto lo stato. Poiché il produttore e il consumatore stanno presumibilmente facendo qualcosa, anche qui il loro stato deve trovarsi nell'interfaccia del chiamante.

main_loop(q, other_state) {
    if (q = Queue.empty) {
        let (new_state, element) = producer(other_state);
        main_loop(Queue.add(q, element), new_state);
    } else {
        let (tail, element) = Queue.take(q);
        let new_state = consumer(other_state, element);
        main_loop(tail, new_state);
    }
}
main_loop(Queue.empty, initial_state)

Nota come ora ogni stato è gestito esplicitamente. Le funzioni di manipolazione della coda prendono una coda come input e producono una nuova coda come output. Anche il produttore e il consumatore passano attraverso il loro stato.

Programmazione concorrente non si adatta così bene all'interno programmazione funzionale, ma si adatta molto bene intorno programmazione funzionale. L'idea è di eseguire un gruppo di nodi di calcolo separati e consentire loro di scambiare messaggi. Ogni nodo esegue un programma funzionale e il suo stato cambia mentre invia e riceve messaggi.

Continuando l'esempio, poiché esiste un'unica coda, è gestita da un nodo particolare. I consumatori inviano a quel nodo un messaggio per ottenere un elemento. I produttori inviano a quel nodo un messaggio per aggiungere un elemento.

main_loop(q) =
    consumer->consume(q->take()) || q->add(producer->produce());
    main_loop(q)

L'unico linguaggio "industrializzato" che ottiene la concorrenza giusta³ è Erlang . Imparare Erlang è sicuramente il percorso verso l'illuminazione sulla programmazione concorrente.

Adesso tutti passano alle lingue senza effetti collaterali!

¹ Questo termine ha diversi significati; qui penso che lo stai usando per indicare la programmazione senza effetti collaterali, e questo è anche il significato che sto usando.
² La programmazione con stato implicito è una programmazione imperativa ; l'orientamento agli oggetti è una preoccupazione completamente ortogonale.
³ Infiammatorio, lo so, ma intendo. I thread con memoria condivisa sono il linguaggio assembly della programmazione concorrente. Il passaggio dei messaggi è molto più semplice da capire e la mancanza di effetti collaterali brilla davvero non appena si introduce la concorrenza.
E questo proviene da qualcuno che non è un fan di Erlang, ma per altri motivi.


2
+1 Risposta molto più completa, anche se suppongo che si possa discutere che Erlang non sia un linguaggio FP puro.
Rein Henrichs,

1
@Rein Henrichs: Davvero. Di fatto, tra tutte le lingue tradizionali attualmente esistenti, Erlang è quella che attua più fedelmente l'Orientamento agli oggetti.
Jörg W Mittag,

2
@ Jörg concordato. Anche se, ancora una volta, si potrebbe pensare che FP e OO puri siano ortogonali.
Rein Henrichs,

Pertanto, per implementare una coda immutabile in un software concorrente, i messaggi devono essere inviati e ricevuti tra i nodi. Dove sono memorizzati i messaggi in sospeso?
mouviciel,

@mouviciel Gli elementi della coda sono memorizzati nella coda dei messaggi in arrivo del nodo. Questa funzione di coda dei messaggi è una funzionalità di base di un'infrastruttura distribuita. Una progettazione di infrastruttura alternativa che funzioni bene per la concorrenza locale ma non con i sistemi distribuiti consiste nel bloccare il mittente fino a quando il destinatario non è pronto. Mi rendo conto che questo non spiega tutto, ci vorrebbe un capitolo o due di un libro sulla programmazione concorrente per spiegarlo completamente.
Gilles 'SO- smetti di essere malvagio' il

4

Il comportamento con stato in una lingua FP è implementato come una trasformazione da uno stato precedente a un nuovo stato. Ad esempio, accodamento sarebbe una trasformazione da una coda e un valore in una nuova coda con il valore accodato. Dequeue sarebbe una trasformazione da una coda a un valore e una nuova coda con il valore rimosso. Costrutti come le monadi sono stati concepiti per astrarre questa trasformazione dello stato (e altri risultati del calcolo) in modi utili


3
Se si tratta di una nuova coda per ogni operazione di aggiunta / rimozione, in che modo due (o più) operazioni asincrone (thread) condividono la coda? È uno schema per sottrarre la novità della coda?
venkram,

La concorrenza è una domanda completamente diversa. Non posso fornire una risposta sufficiente in un commento.
Rein Henrichs,

2
@Rein Henrichs: "non è possibile fornire una risposta sufficiente in un commento". Ciò significa in genere che è necessario aggiornare la risposta per risolvere i problemi relativi ai commenti.
S.Lott

Anche la concorrenza può essere monadica, vedi haskells Control.Concurrency.STM.
alternativa

1
@ S. Lott in questo caso significa che l'OP dovrebbe porre una nuova domanda. La concorrenza è OT a questa domanda, che riguarda le strutture di dati immutabili.
Rein Henrichs,

2

... problemi risolti da rappresentazioni con stato in OOP con immutabili in FP (per esempio: una coda con produttori e consumatori)

La tua domanda è quello che viene chiamato un "problema XY". In particolare, il concetto che citi (in coda con produttori e consumatori) è in realtà una soluzione e non un "problema" come descrivi. Questo introduce una difficoltà perché stai chiedendo un'implementazione puramente funzionale di qualcosa che è intrinsecamente impuro. Quindi la mia risposta inizia con una domanda: qual è il problema che stai cercando di risolvere?

Esistono molti modi in cui più produttori possono inviare i loro risultati a un singolo consumatore condiviso. Forse la soluzione più ovvia in F # è rendere il consumatore un agente (aka MailboxProcessor) e avere i produttori i Postloro risultati con l'agente consumatore. Questo utilizza una coda internamente e non è puro (l'invio di messaggi in F # è un effetto collaterale non controllato, un'impurità).

Tuttavia, è abbastanza probabile che il problema di fondo sia qualcosa di più simile al modello scatter-gather della programmazione parallela. Per risolvere questo problema, è possibile creare una matrice di valori di input e quindi Array.Parallel.mapsu di essi e raccogliere i risultati utilizzando un seriale Array.reduce. In alternativa, è possibile utilizzare le funzioni del PSeqmodulo per elaborare in parallelo gli elementi delle sequenze.

Dovrei anche sottolineare che non c'è nulla di sbagliato nel pensiero statale. La purezza ha dei vantaggi, ma non è certamente una panacea e dovresti anche essere consapevole delle sue carenze. In effetti, questo è esattamente il motivo per cui F # non è un linguaggio funzionale puro: quindi puoi usare le impurità quando sono preferibili.


1

Clojure ha un concetto ben concepito di stato e identità, strettamente correlato alla concorrenza. L'immutabilità svolge un ruolo importante, tutti i valori in Clojure sono immutabili e sono accessibili tramite riferimenti. I riferimenti sono più che semplici puntatori. Gestiscono l'accesso al valore e ce ne sono diversi tipi con semantica diversa. Un riferimento può essere modificato per indicare un nuovo valore (immutabile) e tale cambiamento è garantito come atomico. Tuttavia, dopo la modifica tutti gli altri thread funzionano ancora sul valore originale, almeno fino a quando non accedono nuovamente al riferimento.

Consiglio vivamente di leggere un eccellente articolo sullo stato e l'identità in Clojure , che spiega i dettagli molto meglio di quanto potrei.

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.