Nella programmazione funzionale, le variabili locali mutabili senza effetti collaterali sono ancora considerate "cattive pratiche"?


23

Avere variabili locali mutabili in una funzione che viene utilizzata solo internamente (ad es. La funzione non ha effetti collaterali, almeno non intenzionalmente) è ancora considerata "non funzionale"?

ad es. nel controllo dello stile del corso "Programmazione funzionale con Scala" viene considerato varerrato qualsiasi utilizzo

La mia domanda, se la funzione non ha effetti collaterali, è ancora scoraggiante scrivere codice di stile imperativo?

ad es. invece di usare la ricorsione della coda con il modello di accumulatore, cosa c'è di sbagliato nel fare un ciclo locale per creare un mutabile localeListBuffer e aggiungerlo, purché l'ingresso non sia cambiato?

Se la risposta è "sì, sono sempre scoraggiati, anche se non ci sono effetti collaterali", allora qual è la ragione?


3
Tutti i consigli, le esortazioni ecc. Sull'argomento che abbia mai ascoltato si riferiscono allo stato mutevole condiviso come fonte di complessità. Questo corso è destinato esclusivamente ai principianti? Quindi è probabilmente una semplificazione intenzionale ben intenzionata.
Kilian Foth,

3
@KilianFoth: lo stato mutabile condiviso è un problema in contesti multithread, ma lo stato mutabile non condiviso può portare anche a programmi su cui è difficile ragionare.
Michael Shaw,

1
Penso che usare una variabile locale mutabile non sia necessariamente una cattiva pratica, ma non è uno "stile funzionale": penso che lo scopo del corso Scala (che ho seguito durante lo scorso autunno) sia insegnarti a programmare in uno stile funzionale. Una volta che puoi distinguere chiaramente tra stile funzionale e stile imperativo puoi decidere quando usare quale (nel caso in cui il tuo linguaggio di programmazione lo consenta entrambi). varè sempre non funzionale. Scala ha valori pigri e ottimizzazione della ricorsione della coda, che consentono di evitare completamente i vars.
Giorgio,

Risposte:


17

L'unica cosa che è inequivocabilmente cattiva pratica qui è affermare che qualcosa è una pura funzione quando non lo è.

Se le variabili mutabili vengono utilizzate in un modo veramente e completamente autonomo, la funzione è esternamente pura e tutti sono felici. Haskell infatti lo supporta esplicitamente , con il sistema dei tipi anche garantendo che i riferimenti mutabili non possano essere usati al di fuori della funzione che li crea.

Detto questo, penso che parlare di "effetti collaterali" non sia il modo migliore per vederlo (ed è il motivo per cui ho detto "puro" sopra). Tutto ciò che crea una dipendenza tra la funzione e lo stato esterno rende le cose più difficili da ragionare e ciò include cose come conoscere l'ora corrente o usare lo stato mutabile nascosto in un modo non thread-safe.


16

Il problema non è la mutabilità in sé, è una mancanza di trasparenza referenziale.

Una cosa referenzialmente trasparente e un riferimento ad essa devono sempre essere uguali, quindi una funzione referenzialmente trasparente restituirà sempre gli stessi risultati per un determinato insieme di input e una "variabile" referenzialmente trasparente è in realtà un valore anziché una variabile, poiché non può cambiare. È possibile creare una funzione referenzialmente trasparente con all'interno una variabile mutabile; non è un problema. Tuttavia, potrebbe essere più difficile garantire che la funzione sia referenzialmente trasparente, a seconda di ciò che si sta facendo.

C'è un caso in cui riesco a pensare a dove la mutabilità debba essere usata per fare qualcosa di molto funzionale: la memoizzazione. La memorizzazione nella cache memorizza i valori da una funzione, quindi non devono essere ricalcolati; è referenzialmente trasparente, ma usa la mutazione.

Ma in generale la trasparenza referenziale e l'immutabilità vanno di pari passo, a parte una variabile locale mutabile in una funzione e una memoizzazione referenzialmente trasparenti, non sono sicuro che ci siano altri esempi in cui non è così.


4
Il tuo punto sulla memoizzazione è molto buono. Si noti che Haskell enfatizza fortemente la trasparenza referenziale per la programmazione, ma il comportamento simile alla memoizzazione della valutazione pigra implica una quantità sbalorditiva di mutazione che viene eseguita dal runtime linguistico dietro le quinte.
CA McCann,

@CA McCann: Penso che ciò che dici sia molto importante: in un linguaggio funzionale il runtime può usare la mutazione per ottimizzare il calcolo, ma non esiste un costrutto nel linguaggio che consenta al programmatore di usare la mutazione. Un altro esempio è un ciclo while con una variabile loop: in Haskell puoi scrivere una funzione ricorsiva di coda che può essere implementata con una variabile mutabile (per evitare di usare lo stack), ma ciò che il programmatore vede sono argomenti di funzione immutabili che vengono passati da uno chiama al prossimo.
Giorgio,

@Michael Shaw: +1 per "Il problema non è la mutabilità in sé, è una mancanza di trasparenza referenziale." Forse puoi citare il linguaggio pulito in cui hai tipi di unicità: questi consentono la mutabilità ma garantiscono comunque la trasparenza referenziale.
Giorgio,

@Giorgio: non so davvero nulla di Clean, anche se di tanto in tanto ne ho sentito parlare. Forse dovrei esaminarlo.
Michael Shaw,

@Michael Shaw: non so molto di Clean, ma so che utilizza tipi di unicità per garantire la trasparenza referenziale. Fondamentalmente, è possibile modificare un oggetto dati a condizione che dopo la modifica non ci siano riferimenti al vecchio valore. L'IMO illustra questo punto: la trasparenza referenziale è il punto più importante e l'immutabilità è solo un modo possibile per garantirlo.
Giorgio,

8

Non è davvero bello ridurlo a "buone pratiche" vs "cattive pratiche". Scala supporta valori mutabili perché risolvono alcuni problemi molto meglio dei valori immutabili, vale a dire quelli che sono di natura iterativa.

Per la prospettiva, sono abbastanza sicuro che tramite CanBuildFromquasi tutte le strutture immutabili fornite da Scala facciano una sorta di mutazione internamente. Il punto è che ciò che espongono è immutabile. Mantenere il maggior numero possibile di valori immutabili aiuta a rendere il programma più facile da ragionare e meno soggetto a errori .

Ciò non significa che devi necessariamente evitare strutture e valori mutabili internamente quando hai un problema che si adatta meglio alla mutabilità.

Tenendo presente ciò, molti problemi che richiedono in genere variabili mutabili (come il looping) possono essere risolti meglio con molte delle funzioni di ordine superiore fornite da linguaggi come Scala (map / filter / fold). Sii consapevole di quelli.


2
Sì, non ho quasi mai bisogno di un ciclo for quando uso le collezioni di Scala. map, filter, foldLeftE forEach fare il trucco maggior parte del tempo, ma non quando lo fanno, essendo in grado di sentire che sono "OK" per un ritorno alla forza bruta codice imperativo è bello. (purché non ci siano effetti collaterali, ovviamente)
Eran Medan,

3

A parte potenziali problemi con la sicurezza del thread, in genere perdi anche molto tipo di sicurezza. I loop imperativi hanno un tipo di ritorno Unite possono assumere praticamente qualsiasi espressione per gli input. Le funzioni di ordine superiore e persino la ricorsione hanno semantica e tipi molto più precisi.

Hai anche molte più opzioni per l'elaborazione funzionale dei container rispetto ai loop imperativi. Con imperativo, in pratica hai for, whilee piccole variazioni su quei due come do...whilee foreach.

In funzionale, hai aggregato, conteggio, filtro, trova, flatMap, fold, groupBy, lastIndexWhere, map, maxBy, minBy, partition, scan, sortBy, sortWith, span e takeWhile, solo per citarne alcuni più comuni da Scala libreria standard. Quando ti abitui ad avere quelli disponibili, i forloop imperativi sembrano troppo semplici in confronto.

L'unica vera ragione per usare la mutabilità locale è molto occasionalmente per le prestazioni.


2

Direi che è quasi tutto ok. Inoltre, generare strutture in questo modo potrebbe essere un buon modo per migliorare le prestazioni in alcuni casi. Clojure ha risolto questo problema fornendo strutture di dati transitori .

L'idea di base è consentire le mutazioni locali in un ambito limitato e quindi congelare la struttura prima di restituirla. In questo modo, il tuo utente può ancora ragionare sul tuo codice come se fosse puro, ma sei in grado di eseguire trasformazioni sul posto quando necessario.

Come dice il link:

Se un albero cade nel bosco, emette un suono? Se una funzione pura muta alcuni dati locali per produrre un valore di ritorno immutabile, va bene?


2

Non avere variabili locali modificabili ha un vantaggio: rende la funzione più amichevole nei confronti dei thread.

Sono stato bruciato da una tale variabile locale (non nel mio codice, né avevo l'origine) causando un danneggiamento dei dati a bassa probabilità. La sicurezza del thread non è stata menzionata in un modo o nell'altro, non esisteva uno stato persistente tra le chiamate e non c'erano effetti collaterali. Non mi è venuto in mente che potrebbe non essere sicuro per i thread, inseguire un 1 su 100.000 dati casuali di corruzione è un dolore reale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.