Programmazione funzionale - Immutabilità

11

Sto cercando di capire di gestire i dati immutabili in FP (in particolare in F #, ma anche altri FP sono ok) e di rompere la vecchia abitudine di pensare a pieno titolo (stile OOP). Una parte della risposta selezionata alla domanda qui ha reiterato la mia ricerca di qualsiasi scrittura -up intorno a problemi risolti da rappresentazioni stateful in OOP con immutabili in FP (ad esempio: una coda con Producers & Consumer). Qualche idea o link sono i benvenuti? Grazie in anticipo.

Modifica : per chiarire un po 'la questione, in che modo le strutture immutabili (ex: queue) possono essere condivise contemporaneamente su più thread (es .: produttore e consumatore) in FP

    
posta venkram 18.05.2011 - 22:42
fonte

4 risposte

19

Sebbene a volte sia espresso in questo modo, la programmazione funzionale¹ non impedisce i calcoli statali. Quello 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 pseudolinguaggi):

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 dati di coda funzionale (sempre 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, q stesso è una variabile; gli assegnamenti q := Queue.add(…) e q := tail fanno puntare a un oggetto diverso. L'interfaccia delle funzioni della coda è cambiata: ciascuna deve restituire il nuovo oggetto coda risultante dall'operazione.

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

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 singolo stato viene gestito in modo esplicito. 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 il loro stato.

La programmazione concorrente non si adatta bene alla programmazione funzionale all'interno , ma si adatta molto bene alla programmazione funzionale. L'idea è di eseguire un gruppo di nodi di computazione separati e lasciarli scambiare messaggi. Ogni nodo esegue un programma funzionale e il suo stato cambia quando invia e riceve messaggi.

Continuando con l'esempio, poiché esiste una singola 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 diritto di concorrenza³ è Erlang . L'apprendimento di Erlang è sicuramente il percorso verso l'illuminazione⁴ sulla programmazione concorrente.

Tutti ora passano alle lingue senza effetto collaterale!

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

    
risposta data 19.05.2011 - 00:44
fonte
4

Il comportamento di stato in un linguaggio FP è implementato come una trasformazione da uno stato precedente a un nuovo stato. Ad esempio, enqueue sarebbe una trasformazione da una coda e un valore a 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 ideati per astrarre questa trasformazione dello stato (e altri risultati del calcolo) in modi utili

    
risposta data 18.05.2011 - 22:46
fonte
2

...problems that are solved by stateful representations in OOP with immutable ones in FP (For ex: A queue with Producers & Consumer)

La tua domanda è ciò che viene chiamato un "problema XY". Nello specifico, il concetto che citi (in coda con i produttori e i consumatori) è in realtà una soluzione e non un "problema" come descrivi. Ciò 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?

Ci sono molti modi in cui più produttori possono inviare i loro risultati a un singolo consumatore condiviso. Forse la soluzione più ovvia in F # è quella di rendere il consumatore un agente (alias MailboxProcessor ) e avere i produttori Post i loro risultati all'agente consumatore. Questo usa una coda internamente e non è pura (l'invio di messaggi in F # è un effetto collaterale incontrollato, un'impurità).

Tuttavia, è molto probabile che il problema sottostante sia qualcosa di più simile al pattern scatter-gather della programmazione parallela. Per risolvere questo problema potresti creare una matrice di valori di input e quindi Array.Parallel.map su di essi e raccogliere i risultati utilizzando un Array.reduce seriale. In alternativa, potresti utilizzare le funzioni del modulo PSeq per elaborare gli elementi di sequenze in parallelo.

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

    
risposta data 27.02.2012 - 11:18
fonte
1

Clojure ha un concetto di stato e identità molto ben congegnato, strettamente correlato alla concorrenza. L'immutabilità gioca un ruolo importante, tutti i valori in Clojure sono immutabili e sono accessibili tramite riferimenti. I riferimenti sono più di semplici indicazioni. Gestiscono l'accesso al valore e ne esistono diversi tipi con semantica diversa. Un riferimento può essere modificato in modo da puntare a un nuovo valore (immutabile) e tale modifica è garantita come atomica. Tuttavia, dopo la modifica, tutti gli altri thread funzionano ancora sul valore originale, almeno finché non accedono nuovamente al riferimento.

Ti consiglio vivamente di leggere un eccellente articolo su stato e identità in Clojure , spiega i dettagli molto meglio di quanto potrei.

    
risposta data 19.05.2011 - 01:37
fonte

Leggi altre domande sui tag