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.