La programmazione funzionale include molte tecniche diverse. Alcune tecniche vanno bene con effetti collaterali. Ma un aspetto importante è 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. Ciò rende più facile ragionare sul programma, specialmente quando si esegue il debug.
Se la funzione ha degli effetti collaterali, questo non vale. Il valore restituito non è equivalente alla chiamata alla funzione, poiché il valore restituito non contiene gli effetti collaterali.
La soluzione è smettere di usare effetti side e codificare questi effetti nel valore di ritorno . Diverse lingue hanno diversi sistemi di effetti. Per esempio. Haskell usa le monadi per codificare determinati effetti come la mutazione IO o stato. I linguaggi C / C ++ / Rust hanno un sistema di tipi che non consente la mutazione di alcuni valori.
In una lingua imperativa, una funzione print("foo")
stamperà qualcosa e non restituirà nulla. In un linguaggio funzionale puro come Haskell, una funzione print
accetta 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 tanti nuovi stati dal vecchio stato come mi piace. Tuttavia, solo uno sarà mai usato dalla funzione principale. Quindi ho bisogno di sequenziare gli stati da più azioni concatenando le funzioni. Per stampare foo bar
, potrei dire qualcosa come print "bar" (print "foo" originalState)
.
Se non viene utilizzato uno stato di output, Haskell non esegue le azioni che portano a tale stato, perché è un linguaggio pigro. Al contrario, questa pigrizia è possibile solo perché tutti gli effetti sono codificati esplicitamente come valori di ritorno.
Si noti che Haskell è il linguaggio funzionale comunemente usato solo che utilizza questa rotta. Altre lingue funzionali incl. la famiglia Lisp, la famiglia ML e i nuovi linguaggi funzionali come Scala scoraggiano, ma consentono comunque effetti collaterali - potrebbero essere chiamati linguaggi imperativo-funzionali.
L'uso degli effetti collaterali per I / O probabilmente sta bene. Spesso, I / O (diverso dal logging) viene eseguito solo al limite esterno del sistema. Nessuna comunicazione esterna avviene all'interno della tua logica aziendale. È quindi possibile scrivere il nucleo del software in uno stile puro, mentre si esegue ancora l'I / O impuro in una shell esterna. Ciò significa anche che il core può essere stateless.
L'apolidia ha una serie di vantaggi pratici, come una maggiore ragionevolezza e scalabilità. Questo è molto popolare per i back-end delle applicazioni Web. Ogni stato è tenuto all'esterno, in un database condiviso. Ciò semplifica il bilanciamento del carico: non devo incollare le sessioni su un server specifico. Cosa succede se ho bisogno di più server? Basta aggiungere un altro, perché sta usando lo stesso database. Cosa succede se un server si arresta in modo anomalo? Posso ripetere qualsiasi richiesta in sospeso su un altro server. Certo, c'è ancora stato - nel database. Ma l'ho reso esplicito e l'ho estratto, e potrei usare internamente un approccio puramente funzionale se voglio.