La programmazione funzionale include molte tecniche diverse. Alcune tecniche vanno bene con gli effetti collaterali. Ma un aspetto importante è il ragionamento equazionale : se chiamo una funzione sullo stesso valore, ottengo sempre lo stesso risultato. Quindi posso sostituire una chiamata di funzione con il valore restituito e ottenere un comportamento equivalente. Questo rende più facile ragionare sul programma, specialmente durante il debug.
Se la funzione ha effetti collaterali, questo non regge. Il valore restituito non equivale alla chiamata della funzione, poiché il valore restituito non contiene gli effetti collaterali.
La soluzione è smettere di usare gli effetti collaterali e codificare questi effetti nel valore restituito . Lingue diverse hanno sistemi di effetti diversi. Ad esempio, Haskell usa monadi per codificare alcuni effetti come IO o mutazione di stato. I linguaggi C / C ++ / Rust hanno un sistema di tipi che può impedire la mutazione di alcuni valori.
In un linguaggio imperativo, una print("foo")
funzione stampa qualcosa e non restituisce nulla. In un linguaggio funzionale puro come Haskell, una print
funzione prende anche un oggetto che rappresenta lo stato del mondo esterno e restituisce un nuovo oggetto che rappresenta lo stato dopo aver eseguito questo output. Qualcosa di simile a newState = print "foo" oldState
. Posso creare tutti i nuovi stati del vecchio stato che mi piacciono. Tuttavia, solo uno verrà mai utilizzato dalla funzione principale. Quindi ho bisogno di sequenziare gli stati da più azioni concatenando le funzioni. Per stampare foo bar
, potrei dire qualcosa del genere print "bar" (print "foo" originalState)
.
Se non viene utilizzato uno stato di output, Haskell non esegue le azioni che portano a quello stato, perché è un linguaggio pigro. Al contrario, questa pigrizia è possibile solo perché tutti gli effetti sono esplicitamente codificati come valori di ritorno.
Si noti che Haskell è l' unico linguaggio funzionale comunemente usato che utilizza questa route. Altre lingue funzionali incl. la famiglia Lisp, la famiglia ML e i linguaggi funzionali più recenti come la Scala scoraggiano ma consentono ancora effetti collaterali - potrebbero essere chiamati linguaggi imperativi-funzionali.
L'uso degli effetti collaterali per l'I / O è probabilmente buono. Spesso, l'I / O (diverso dalla registrazione) viene eseguito solo al limite esterno del sistema. Nessuna comunicazione esterna avviene all'interno della logica aziendale. È quindi possibile scrivere il core del software in uno stile puro, pur eseguendo I / O impuri in una shell esterna. Ciò significa anche che il nucleo può essere apolide.
L'apolidia ha una serie di vantaggi pratici, come una maggiore ragionevolezza e scalabilità. Questo è molto popolare per i backend delle applicazioni web. Qualsiasi stato viene mantenuto all'esterno, in un database condiviso. Ciò semplifica il bilanciamento del carico: non devo incollare sessioni a un server specifico. E se avessi bisogno di più server? Basta aggiungerne un altro, perché utilizza lo stesso database. Cosa succede se un server si arresta in modo anomalo? Posso ripetere qualsiasi richiesta in sospeso su un altro server. Naturalmente, c'è ancora stato - nel database. Ma l'ho reso esplicito ed estratto, e potrei usare internamente un puro approccio funzionale se lo desidero.