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, q
essa stessa è una variabile; le assegnazioni q := Queue.add(…)
e q := tail
farle 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.