Tutti i linguaggi funzionali usano la garbage collection?


32

Esiste un linguaggio funzionale che consente di utilizzare la semantica dello stack - distruzione deterministica automatica alla fine dell'ambito?


La distruzione deterministica è davvero utile solo con effetti collaterali. Nel contesto della pura programmazione funzionale, ciò significa semplicemente garantire che determinate azioni (monadiche) vengano sempre eseguite alla fine di una sequenza. Per inciso, è facile scrivere un linguaggio concatenativo funzionale che non necessita di garbage collection.
Jon Purdy

Sono interessato all'argomento della domanda, cosa c'entra con l'altro?
Mattnz,

1
In un linguaggio funzionale senza garbage collection, non vedo come sia possibile la condivisione strutturale di strutture di dati immutabili. Potrebbe essere possibile creare un linguaggio del genere, ma non è quello che userei.
dan_waterworth,

Rust ha molte caratteristiche comunemente identificate come "funzionali" (almeno, sono comunemente ricercate nelle lingue non funzionali come caratteristiche funzionali). Sono curioso di sapere cosa manca. Immut di default, chiusure, funzioni che passano, sovraccarico di principio, ADT (ancora nessun GADT), pattern matching, tutto senza GC. Cos'altro?
Noein,

Risposte:


10

Non che io sappia, anche se non sono un esperto di programmazione funzionale.

In linea di principio sembra piuttosto difficile, poiché i valori restituiti dalle funzioni possono contenere riferimenti ad altri valori che sono stati creati (nello stack) all'interno della stessa funzione o potrebbero essere stati passati facilmente come parametro o referenziati da qualcosa passato come parametro In C, questo problema viene risolto consentendo che si possano verificare puntatori penzolanti (o, più precisamente, comportamento indefinito) se il programmatore non riesce a sistemare le cose. Questo non è il tipo di soluzione approvata dai progettisti di linguaggi funzionali.

Ci sono soluzioni potenziali, comunque. Un'idea è di rendere la durata del valore una parte del tipo di valore, insieme a riferimenti ad esso, e definire regole basate sul tipo che impediscono la restituzione o la referenziazione di qualcosa da restituito da valori a funzione. Non ho elaborato le implicazioni, ma sospetto che sarebbe orribile.

Per il codice monadico, c'è un'altra soluzione che è (in realtà o quasi) anche monadica, e potrebbe dare una sorta di IORef automaticamente distrutto deterministicamente. Il principio è definire azioni di "nidificazione". Se combinati (usando un operatore associativo), definiscono un flusso di controllo di annidamento - penso "elemento XML", con la parte più a sinistra dei valori che fornisce la coppia esterna di tag di inizio e fine. Questi "tag XML" stanno solo definendo l'ordinamento delle azioni monadiche ad un altro livello di astrazione.

Ad un certo punto (sul lato destro della catena di composizione associativa) hai bisogno di una sorta di terminatore per terminare l'annidamento - qualcosa per riempire il buco nel mezzo. La necessità di un terminatore è ciò che probabilmente significa che l'operatore della composizione di annidamento non è monadico, anche se, di nuovo, non sono del tutto sicuro poiché non ho elaborato i dettagli. Come tutto l'applicazione del terminatore fa convertire un'azione di annidamento in efficacemente una normale azione monadica composta, forse no - non influisce necessariamente sull'operatore della composizione di annidamento.

Molte di queste azioni speciali avrebbero un passo "end-tag" nullo e equiparerebbero il passo "begin-tag" a qualche semplice azione monadica. Ma alcuni rappresenterebbero dichiarazioni variabili. Rappresenterebbero il costruttore con il tag di inizio e il distruttore con il tag di fine. Quindi ottieni qualcosa come ...

act = terminate ((def-var "hello" ) >>>= \h ->
                 (def-var " world") >>>= \w ->
                 (use-val ((get h) ++ (get w)))
                )

Traducendo in una composizione monadica con il seguente ordine di esecuzione, ogni tag (non elemento) diventa una normale azione monadica ...

<def-var val="hello">  --  construction
  <def-var val=" world>  --  construction
    <use-val ...>
      <terminator/>
    </use-val>  --  do nothing
  </def-val>  --  destruction
</def-val>  --  destruction

Regole come questa potrebbero consentire l'implementazione di RAII in stile C ++. I riferimenti simil-IORef non possono sfuggire al loro scopo, per ragioni simili al perché i normali IORef non possono sfuggire alla monade - le regole della composizione associativa non forniscono alcun modo per sfuggire al riferimento.

EDIT - Ho quasi dimenticato di dire - c'è un'area definita di cui non sono sicuro. È importante assicurarsi che una variabile esterna non possa fare riferimento a una variabile interna, in sostanza, quindi devono esserci delle restrizioni su ciò che è possibile fare con questi riferimenti simili a IORef. Ancora una volta, non ho elaborato tutti i dettagli.

Pertanto, la costruzione potrebbe ad esempio aprire un file che chiude la distruzione. La costruzione potrebbe aprire una presa chiusa dalla distruzione. Fondamentalmente, come in C ++, le variabili diventano gestori di risorse. Ma a differenza del C ++, non ci sono oggetti allocati in heap che non possono essere distrutti automaticamente.

Sebbene questa struttura supporti RAII, è comunque necessario un Garbage Collector. Sebbene un'azione di nidificazione possa allocare e liberare memoria, trattandola come una risorsa, ci sono ancora tutti i riferimenti a valori funzionali (potenzialmente condivisi) all'interno di quel pezzo di memoria e altrove. Dato che la memoria potrebbe essere semplicemente allocata nello stack, evitando la necessità di un heap gratuito, il vero significato (se presente) è per altri tipi di gestione delle risorse.

Quindi ciò che ottiene è separare la gestione delle risorse in stile RAII dalla gestione della memoria, almeno nel caso in cui RAII si basi su un semplice ambito di nidificazione. È ancora necessario un garbage collector per la gestione della memoria, ma si ottiene una pulizia deterministica automatica sicura e tempestiva di altre risorse.


Non riesco a capire perché un GC sia necessario in tutti i linguaggi funzionali. se si dispone di un framework RAII in stile C ++, il compilatore può utilizzare anche quel meccanismo. I valori condivisi non sono un problema per i framework RAII (vedi C ++ shared_ptr<>), si mantiene comunque la distruzione deterministica. L'unica cosa difficile per RAII sono riferimenti ciclici; RAII funziona in modo pulito se il grafico della proprietà è un grafico aciclico diretto.
Salterio del

Il fatto è che lo stile di programmazione funzionale è praticamente costruito attorno a chiusure / lambda / funzioni anonime. Senza GC, non hai la stessa libertà di usare le tue chiusure, quindi il tuo linguaggio diventa significativamente meno funzionale.
comingstorm

@comingstorm - C ++ ha lambdas (a partire da C ++ 11) ma nessun garbage collector standard. Le lambda portano anche il loro ambiente in una chiusura - e gli elementi in quell'ambiente possono essere passati per riferimento, così come la possibilità che i puntatori vengano passati per valore. Ma come ho scritto nel mio secondo paragrafo, C ++ consente la possibilità di puntatori penzolanti - sono i programmatori (piuttosto che i compilatori o gli ambienti di runtime) a garantire che ciò non accada mai.
Steve314,

@MSalters: ci sono costi per garantire che non sia mai possibile creare cicli di riferimento. Pertanto, è almeno non banale rendere la lingua responsabile di tale restrizione. L'assegnazione a un puntatore probabilmente diventa un'operazione a tempo non costante. Anche se in alcuni casi potrebbe essere comunque l'opzione migliore. La garbage collection evita questo problema, con costi diversi. Rendere responsabile il programmatore è un altro. Non c'è una ragione forte per cui i puntatori penzolanti dovrebbero essere OK nei linguaggi imperativi ma non funzionali, ma ancora non consiglio di scrivere puntatore-penzoloni-Haskell.
Steve314,

Direi che la gestione manuale della memoria significa che non hai la stessa libertà di usare le chiusure C ++ 11 delle chiusure Lisp o Haskell. (In realtà sono abbastanza interessato a comprendere i dettagli di questo compromesso, in quanto vorrei scrivere un linguaggio di programmazione di sistemi funzionali ...)
comingstorm

3

Se consideri C ++ come un linguaggio funzionale (ha lambdas), allora è un esempio di un linguaggio che non usa una garbage collection.


8
Che cosa succede se non consideri C ++ come un linguaggio funzionale (IMHO non lo è, anche se puoi scrivere un programma funzionale con esso, puoi anche scrivere alcuni programmi estremamente non-funzionanti (funzionanti ....) con esso)
Mattnz,

@mattnz Quindi immagino che la risposta non si applichi. Non sono sicuro di cosa accada in altre lingue (come ad esempio haskel)
BЈовић

9
Dire che C ++ è funzionale è come dire che Perl è orientato agli oggetti ...
Dinamica

Almeno i compilatori c ++ possono verificare gli effetti collaterali. (via const)
tp1

@ tp1 - (1) Spero che questo non regredisca nella lingua migliore, e (2) non è proprio vero. Innanzitutto, gli effetti veramente importanti sono principalmente I / O. Secondo, anche per effetti sulla memoria mutabile, const non li blocca. Anche se si presume che non vi sia alcuna possibilità di sovvertire il typeystem (generalmente ragionevole in C ++), c'è il problema della costanza logica e della parola chiave "++" mutabile. Fondamentalmente, puoi ancora avere mutazioni nonostante const. Ci si aspetta che il risultato sia "logicamente" lo stesso, ma non necessariamente la stessa rappresentazione.
Steve314

2

Devo dire che la domanda è un po 'mal definita perché assume che esista una raccolta standard di "linguaggi funzionali". Quasi ogni linguaggio di programmazione supporta una certa quantità di programmazione funzionale. E quasi ogni linguaggio di programmazione supporta una certa quantità di programmazione imperativa. Dove si disegna la linea per dire quale sia un linguaggio funzionale e quale sia un linguaggio imperativo, oltre che guidato da pregiudizi culturali e dogma popolare?

Un modo migliore per formulare la domanda sarebbe "è possibile supportare la programmazione funzionale in una memoria allocata nello stack". La risposta è, come già accennato, molto difficile. Lo stile di programmazione funzionale promuove l'allocazione di strutture di dati ricorsive a piacimento, il che richiede una memoria heap (sia che si raccolga la spazzatura sia che si contino i riferimenti). Tuttavia, esiste una tecnica di analisi del compilatore piuttosto sofisticata chiamata analisi della memoria basata sulla regione che utilizza il compilatore per dividere l'heap in blocchi di grandi dimensioni che possono essere allocati e deallocati automaticamente, in modo simile all'allocazione dello stack. La pagina di Wikipedia elenca varie implementazioni della tecnica, sia per linguaggi "funzionali" che "imperativi".


1
Il conteggio dei riferimenti IMO è la garbage collection e avere un heap non implica comunque che queste siano le uniche opzioni. C malloc e mfrees usano un heap, ma non ha un garbage collector (standard) e conta solo i riferimenti se si scrive il codice per farlo. Il C ++ è quasi lo stesso - ha (come standard, in C ++ 11) puntatori intelligenti con conteggio dei riferimenti incorporato, ma puoi ancora fare nuovi manuali ed eliminare se ne hai davvero bisogno.
Steve314

Un motivo comune per affermare che il conteggio dei riferimenti non è la garbage collection è che non riesce a raccogliere i cicli di riferimento. Questo vale certamente per implementazioni semplici (probabilmente includendo i puntatori intelligenti C ++ - non ho controllato) ma non è sempre così. Almeno una macchina virtuale Java (di IBM, IIRC) ha utilizzato il conteggio dei riferimenti come base per la propria garbage collection.
Steve314
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.