it-swarm.it

Perché gli effetti collaterali sono considerati malvagi nella programmazione funzionale?

Sento che gli effetti collaterali sono un fenomeno naturale. Ma è qualcosa di simile al tabù nei linguaggi funzionali. Quali sono le ragioni?

La mia domanda è specifica per lo stile di programmazione funzionale. Non tutti i linguaggi/paradigmi di programmazione.

70
Gulshan

Scrivere le tue funzioni/metodi senza effetti collaterali - quindi sono funzioni pure - rende più facile ragionare sulla correttezza del tuo programma.

Inoltre semplifica la composizione di tali funzioni per creare nuovi comportamenti.

Rende anche possibili alcune ottimizzazioni, in cui il compilatore può ad esempio memorizzare i risultati delle funzioni o utilizzare Common Subexpression Elimination.

Modifica: su richiesta di Benjol: poiché gran parte del tuo stato è archiviato nello stack (flusso di dati, non flusso di controllo, come lo ha definito Jonas qui ), puoi parallelizzare o riordinare in altro modo l'esecuzione di quelle parti del tuo calcolo che sono indipendenti l'uno dall'altro. Puoi facilmente trovare quelle parti indipendenti perché una parte non fornisce input all'altra.

In ambienti con debugger che ti consentono di ripristinare lo stack e riprendere il calcolo (come Smalltalk), avere funzioni pure significa che puoi facilmente vedere come cambia un valore, perché gli stati precedenti sono disponibili per l'ispezione. In un calcolo ricco di mutazione, a meno che non si aggiungano esplicitamente azioni do/annulla alla struttura o all'algoritmo, non è possibile visualizzare la cronologia del calcolo. (Ciò si ricollega al primo paragrafo: la scrittura di funzioni pure semplifica ispezionare la correttezza del programma.)

73
Frank Shearar

Hai sbagliato, la programmazione funzionale promuove la limitazione degli effetti collaterali per rendere i programmi facili da capire e ottimizzare. Anche Haskell ti permette di scrivere su file.

In sostanza quello che sto dicendo è che i programmatori funzionali non pensano che gli effetti collaterali siano cattivi, pensano semplicemente che limitare l'uso degli effetti collaterali sia buono. So che può sembrare una distinzione così semplice ma fa la differenza.

24
ChaosPandion

Da un articolo su Programmazione funzionale :

In pratica, le applicazioni devono avere alcuni effetti collaterali. Simon Peyton-Jones, un importante collaboratore del linguaggio di programmazione funzionale Haskell, ha dichiarato quanto segue: "Alla fine, qualsiasi programma deve manipolare lo stato. Un programma che non ha effetti collaterali è una specie di scatola nera. Tutto ciò che puoi dire è che la scatola diventa più calda ". ( http://oscon.blip.tv/file/324976 ) La chiave è limitare gli effetti collaterali, identificarli chiaramente ed evitare di disperderli nel codice.

23
Peter Stuifzand

Alcune note:

  • Le funzioni senza effetti collaterali possono essere eseguite in modo banale in parallelo, mentre le funzioni con effetti collaterali richiedono in genere una sorta di sincronizzazione.

  • Le funzioni senza effetti collaterali consentono un'ottimizzazione più aggressiva (ad esempio, utilizzando in modo trasparente una cache dei risultati), perché finché otteniamo il risultato giusto, non importa nemmeno se la funzione fosse davvero eseguito

13
user281377

Lavoro principalmente nel codice funzionale ora e da quella prospettiva sembra accecantemente ovvio. Gli effetti collaterali creano un enorme onere mentale per i programmatori che cercano di leggere e comprendere il codice. Non noti questo onere fino a quando non ti liberi da un po ', quindi improvvisamente devi leggere di nuovo il codice con effetti collaterali.

Considera questo semplice esempio:

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

In un linguaggio funzionale, io so che foo è ancora 42. Non devo nemmeno guarda il codice in mezzo, tanto meno capiscilo, o guarda le implementazioni delle funzioni che chiama.

Tutto ciò che riguarda la concorrenza, la parallelizzazione e l'ottimizzazione è bello, ma è quello che gli scienziati informatici hanno messo sulla brochure. Non doversi chiedermi chi sta mutando la tua variabile e quando è ciò che mi piace davvero nella pratica quotidiana.

11
Karl Bielefeldt

Poche o nessuna lingua rende impossibile causare effetti collaterali. Le lingue completamente prive di effetti collaterali sarebbero proibitivamente difficili (quasi impossibili) da usare, se non con una capacità molto limitata.

Perché gli effetti collaterali sono considerati malvagi?

Perché rendono molto più difficile ragionare esattamente su ciò che fa un programma e dimostrare che fa ciò che ti aspetti che faccia.

A un livello molto alto, immagina di testare un intero sito Web a 3 livelli con solo test black-box. Certo, è fattibile, a seconda della scala. Ma ci sono sicuramente molte duplicazioni in corso. E se c'è è un bug (che è correlato a un effetto collaterale), allora potresti potenzialmente rompere l'intero sistema per ulteriori test, fino a quando il bug non viene diagnosticato e corretto e la correzione è distribuito nell'ambiente di test.

Vantaggi

Ora ridimensionalo. Se fossi abbastanza bravo a scrivere codice gratuito con effetti collaterali, quanto saresti più veloce a ragionare su ciò che ha fatto un codice esistente? Quanto più velocemente potresti scrivere unit test? Quanto ti sentiresti sicuro che il codice senza effetti collaterali fosse garantito privo di bug e che gli utenti potessero limitare la loro esposizione a qualsiasi bug che aveva?

Se il codice non ha effetti collaterali, il compilatore potrebbe anche avere ulteriori ottimizzazioni che potrebbe eseguire. Potrebbe essere molto più semplice implementare tali ottimizzazioni. Potrebbe essere molto più semplice anche concettualizzare un'ottimizzazione per il codice libero con effetti collaterali, il che significa che il fornitore del compilatore potrebbe implementare ottimizzazioni che sono difficili da impossibili a impossibili nel codice con effetti collaterali.

La concorrenza è inoltre drasticamente più semplice da implementare, generare automaticamente e ottimizzare quando il codice non ha effetti collaterali. Questo perché tutti i pezzi possono essere valutati in modo sicuro in qualsiasi ordine. Consentire ai programmatori di scrivere codice altamente concorrenziale è ampiamente considerata la prossima grande sfida che l'Informatica deve affrontare e una delle poche rimanenti coperture contro Legge di Moore .

6

Gli effetti collaterali sono come "perdite" nel tuo codice che dovranno essere gestite in seguito, da te o da un collega ignaro.

I linguaggi funzionali evitano variabili di stato e dati mutabili come modo per rendere il codice meno dipendente dal contesto e più modulare. La modularità assicura che il lavoro di uno sviluppatore non influenzerà/minerà il lavoro di un altro.

Il ridimensionamento del tasso di sviluppo in base alle dimensioni del team è oggi un "santo graal" dello sviluppo software. Quando si lavora con altri programmatori, poche cose sono importanti quanto la modularità. Anche il più semplice degli effetti collaterali logici rende la collaborazione estremamente difficile.

4
Ami

Bene, IMHO, questo è abbastanza ipocrita. A nessuno piacciono gli effetti collaterali, ma tutti ne hanno bisogno.

Ciò che è così pericoloso riguardo agli effetti collaterali è che se si chiama una funzione, ciò potrebbe avere un effetto non solo sul modo in cui la funzione si comporta quando viene chiamata la prossima volta, ma probabilmente ha questo effetto su altre funzioni. Pertanto, gli effetti collaterali introducono comportamenti imprevedibili e dipendenze non banali.

I paradigmi di programmazione come OO e funzionale risolvono entrambi questo problema. OO riduce il problema imponendo una separazione di preoccupazioni. Ciò significa che lo stato dell'applicazione, che consiste in molti dati mutabili sono incapsulati in oggetti, ognuno dei quali è responsabile solo del mantenimento del proprio stato, in questo modo il rischio di dipendenze è ridotto e i problemi sono molto più isolati e più facili da rintracciare.

La programmazione funzionale ha un approccio molto più radicale, in cui lo stato dell'applicazione è semplicemente immutabile dal punto di vista del programmatore. Questa è una bella idea, ma rende la lingua inutile da sola. Perché? Perché QUALSIASI operazione di I/O ha effetti collaterali. Non appena si legge da qualsiasi flusso di input, è probabile che lo stato dell'applicazione cambi, poiché la prossima volta che si invoca la stessa funzione, è probabile che il risultato sia diverso. È possibile che tu stia leggendo dati diversi o - anche una possibilità - l'operazione potrebbe non riuscire. Lo stesso vale per l'output. Anche l'output è un'operazione con effetti collaterali. Al giorno d'oggi questo non è ciò che realizzi spesso, ma immagina di avere solo 20 KB per l'output e se l'output è maggiore, la tua app si arresta in modo anomalo perché sei a corto di spazio su disco o altro.

Quindi sì, gli effetti collaterali sono cattivi e pericolosi dal punto di vista di un programmatore. La maggior parte dei bug deriva dal modo in cui alcune parti dello stato dell'applicazione sono interconnesse in modo quasi oscuro, attraverso effetti collaterali non considerati e spesso inutili. Dal punto di vista di un utente, gli effetti collaterali sono il punto di usare un computer. A loro non importa cosa succede dentro o come è organizzato. Fanno qualcosa e si aspettano che il computer cambi di conseguenza.

4
back2dos

Qualsiasi effetto collaterale introduce parametri di input/output extra che devono essere presi in considerazione durante il test.

Ciò rende la convalida del codice molto più complessa in quanto l'ambiente non può essere limitato al solo codice in fase di convalida, ma deve portare in tutto o in parte l'ambiente circostante (il globale che viene aggiornato vive in quel codice laggiù, che a sua volta dipende da quello codice, che a sua volta dipende dal vivere all'interno di un intero Java EE ....)

Cercando di evitare effetti collaterali si limita la quantità di esternalismo necessaria per eseguire il codice.

2
user1249

Nella mia esperienza, un buon design nella programmazione orientata agli oggetti impone l'uso di funzioni che hanno effetti collaterali.

Ad esempio, prendi un'applicazione desktop UI di base. Potrei avere un programma in esecuzione che ha sul suo heap un oggetto grafico che rappresenta lo stato corrente del modello di dominio del mio programma. I messaggi arrivano agli oggetti in quel grafico (ad esempio, tramite le chiamate di metodi invocate dal controller del livello dell'interfaccia utente). Il grafico a oggetti (modello di dominio) sull'heap viene modificato in risposta ai messaggi. Gli osservatori del modello vengono informati di eventuali cambiamenti, l'interfaccia utente e forse altre risorse vengono modificate.

Lungi dall'essere malvagi, la disposizione corretta di questi effetti collaterali che modificano l'heap e che modificano lo schermo sono al centro di OO (in questo caso il modello MVC).

Naturalmente, ciò non significa che i tuoi metodi dovrebbero avere effetti collaterali arbitrari. E le funzioni senza effetti collaterali hanno un ruolo nel migliorare la leggibilità e talvolta le prestazioni del tuo codice.

1
flamingpenguin

Come hanno sottolineato le domande precedenti, i linguaggi funzionali non impediscono così tanto di evitare che il codice abbia effetti collaterali in quanto ci forniscono strumenti per gestire quali effetti collaterali possono accade in un dato codice e quando.

Questo risulta avere conseguenze molto interessanti. Innanzitutto, e ovviamente, ci sono numerose cose che puoi fare con il codice libero ad effetto collaterale, che sono già state descritte. Ma ci sono altre cose che possiamo fare anche quando lavoriamo con il codice che ha effetti collaterali:

  • Nel codice con stato modificabile, possiamo gestire l'ambito dello stato in modo da garantire staticamente che non possa fuoriuscire al di fuori di una determinata funzione, questo ci consente di raccogliere immondizia senza contare i riferimenti o schemi di stile mark-and-sweep , ma assicurati comunque che non rimangano riferimenti. Le stesse garanzie sono utili anche per mantenere informazioni sensibili sulla privacy, ecc. (Questo può essere ottenuto usando la monade ST in haskell)
  • Quando si modifica lo stato condiviso in più thread, è possibile evitare la necessità di blocchi monitorando le modifiche ed eseguendo un aggiornamento atomico al termine di una transazione, oppure eseguendo il rollback della transazione e ripetendola se un altro thread ha apportato una modifica in conflitto. Ciò è possibile solo perché possiamo garantire che il codice non abbia effetti diversi dalle modifiche dello stato (che possiamo abbandonare felicemente). Questo viene eseguito dalla monade STM (Software Transactional Memory) di Haskell.
  • possiamo tracciare gli effetti del codice e insignificarlo banalmente, filtrando tutti gli effetti che potrebbe essere necessario per garantire la sua sicurezza, consentendo (ad esempio) il codice immesso dall'utente di essere eseguito in modo sicuro su un sito web
0
Jules

Il male è un po 'esagerato .. tutto dipende dal contesto dell'uso della lingua.

Un'altra considerazione a quelle già menzionate è che rende le prove della correttezza di un programma molto più semplici se non ci sono effetti collaterali funzionali.

0
Ilan

In basi di codice complesse, le interazioni complesse di effetti collaterali sono la cosa più difficile che trovo a ragionare. Posso solo parlare personalmente dato il modo in cui funziona il mio cervello. Effetti collaterali e stati persistenti e input mutanti e così via mi fanno pensare a "quando" e "dove" le cose accadono per ragionare sulla correttezza, non solo "cosa" sta accadendo in ogni singola funzione.

Non posso semplicemente concentrarmi su "cosa". Non posso concludere dopo aver testato a fondo una funzione che provoca effetti collaterali che diffonderà un'aria di affidabilità in tutto il codice che lo utilizza, poiché i chiamanti potrebbero ancora utilizzarlo in modo errato chiamandolo al momento sbagliato, dal thread errato, nell'errato ordine. Nel frattempo una funzione che non provoca effetti collaterali e restituisce semplicemente un nuovo output dato un input (senza toccarlo) è praticamente impossibile da usare in questo modo.

Ma io sono un tipo pragmatico, penso, o almeno provo a esserlo, e non penso che dobbiamo necessariamente eliminare tutti gli effetti collaterali al minimo indispensabile per ragionare sulla correttezza del nostro codice (almeno Lo troverei molto difficile da fare in lingue come C). Dove trovo molto difficile ragionare sulla correttezza è quando abbiamo la combinazione di complessi flussi di controllo ed effetti collaterali.

Flussi di controllo complessi per me sono quelli che sono di natura grafica, spesso ricorsivi o ricorsivi (code di eventi, ad esempio, che non chiamano direttamente gli eventi in modo ricorsivo ma sono "simili a ricorsivi" in natura), forse fanno cose nel processo di attraversamento di un'effettiva struttura di un grafico collegato o nell'elaborazione di una coda di eventi non omogenea che contiene una miscela eclettica di eventi da elaborare che ci conduce a tutti i tipi di diverse parti della base di codice e che innesca tutti i diversi effetti collaterali. Se provassi a disegnare tutti i posti che alla fine finirai nel codice, assomiglierebbe a un grafico complesso e potenzialmente con nodi nel grafico che non ti saresti mai aspettato sarebbero stati lì in quel dato momento, e dato che sono tutti causando effetti collaterali, ciò significa che potresti non essere solo sorpreso di quali funzioni vengono chiamate, ma anche di quali effetti collaterali si verificano durante quel periodo e l'ordine in cui si verificano.

I linguaggi funzionali possono avere flussi di controllo estremamente complessi e ricorsivi, ma il risultato è così facile da comprendere in termini di correttezza perché non ci sono tutti i tipi di effetti collaterali eclettici che si verificano nel processo. È solo quando complessi flussi di controllo incontrano effetti collaterali eclettici che trovo che induca il mal di testa a cercare di comprendere tutto ciò che sta accadendo e se farà sempre la cosa giusta.

Quindi, quando ho questi casi, trovo spesso molto difficile, se non impossibile, sentirmi molto fiducioso sulla correttezza di tale codice, figuriamoci molto fiducioso di poter apportare modifiche a tale codice senza inciampare in qualcosa di inaspettato. Quindi la soluzione per me è quella di semplificare il flusso di controllo o minimizzare/unificare gli effetti collaterali (unificando, intendo come causare un solo tipo di effetto collaterale a molte cose durante una particolare fase del sistema, non due o tre o un dozzina). Ho bisogno che accada una di queste due cose per consentire al mio cervello semplice di sentirsi sicuro della correttezza del codice esistente e della correttezza dei cambiamenti che presento. È abbastanza facile essere sicuri della correttezza del codice che introduce effetti collaterali se gli effetti collaterali sono uniformi e semplici insieme al flusso di controllo, in questo modo:

for each pixel in an image:
    make it red

È abbastanza facile ragionare sulla correttezza di tale codice, ma principalmente perché gli effetti collaterali sono così uniformi e il flusso di controllo è così semplice. Ma diciamo che avevamo un codice come questo:

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove Edge
         remove vertex

Quindi questo è pseudocodice ridicolmente semplificato che in genere implicherebbe molte più funzioni e cicli annidati e molte altre cose che dovrebbero andare avanti (aggiornamento di più mappe di trama, pesi ossei, stati di selezione, ecc.), Ma anche lo pseudocodice rende così difficile ragione della correttezza a causa dell'interazione del complesso flusso di controllo simile a un grafico e degli effetti collaterali in corso. Quindi una strategia per semplificare è quella di rinviare l'elaborazione e concentrarsi solo su un tipo di effetto collaterale alla volta:

for each vertex to remove:
     mark connected edges
for each marked Edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked Edge:
     remove Edge
for each vertex to remove:
     remove vertex

... qualcosa in tal senso come una ripetizione di semplificazione. Ciò significa che stiamo attraversando i dati più volte, il che comporta sicuramente un costo computazionale, ma spesso troviamo che possiamo multithreading di tale codice risultante più facilmente, ora che gli effetti collaterali e i flussi di controllo hanno assunto questa natura uniforme e più semplice. Inoltre, ogni loop può essere reso più compatibile con la cache rispetto al attraversamento del grafico collegato e causando effetti collaterali mentre procediamo (es: utilizzare un set di bit parallelo per contrassegnare ciò che deve essere attraversato in modo che possiamo quindi fare i passaggi differiti in ordine sequenziale ordinato utilizzando maschere di bit e FFS). Ma soprattutto, trovo che la seconda versione sia molto più facile da ragionare in termini di correttezza e di cambiamento senza causare bug. Quindi è così che mi avvicino comunque e applico lo stesso tipo di mentalità per semplificare l'elaborazione della mesh qui sopra come semplificare la gestione degli eventi e così via - loop più omogenei con flussi di controllo semplici morti che causano effetti collaterali uniformi.

Dopotutto, abbiamo bisogno che si verifichino effetti collaterali ad un certo punto, altrimenti avremmo solo funzioni che trasmettono dati senza un posto dove andare. Spesso dobbiamo registrare qualcosa su un file, visualizzare qualcosa su uno schermo, inviare i dati tramite un socket, qualcosa di questo tipo e tutte queste cose sono effetti collaterali. Ma possiamo sicuramente ridurre il numero di effetti collaterali superflui che si verificano e anche ridurre il numero di effetti collaterali che si verificano quando i flussi di controllo sono molto complicati, e penso che sarebbe molto più facile evitare i bug se lo facessimo.

0
user204677