Esiste un linguaggio funzionale che consente di utilizzare la semantica dello stack - distruzione deterministica automatica alla fine dell'ambito?
Esiste un linguaggio funzionale che consente di utilizzare la semantica dello stack - distruzione deterministica automatica alla fine dell'ambito?
Risposte:
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.
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.
Se consideri C ++ come un linguaggio funzionale (ha lambdas), allora è un esempio di un linguaggio che non usa una garbage collection.
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".