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 nel momento sbagliato, dal thread sbagliato, nel modo sbagliato ordine. Nel frattempo una funzione che non causa effetti collaterali e restituisce semplicemente un nuovo output dato un input (senza toccarlo) è praticamente impossibile da usare in questo modo.
Ma 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 "di tipo ricorsivo" 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 porta a tutti i tipi di diverse parti della base di codice e tutti a innescare 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,
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 l'insieme di 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. Così che'
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 attraverso 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.