La raccolta dei rifiuti è necessaria per l'implementazione di chiusure sicure?


14

Di recente ho frequentato un corso online sui linguaggi di programmazione in cui, tra gli altri concetti, sono state presentate le chiusure. Scrivo due esempi ispirati a questo corso per fornire un contesto prima di porre la mia domanda.

Il primo esempio è una funzione SML che produce un elenco dei numeri da 1 a x, dove x è il parametro della funzione:

fun countup_from1 (x: int) =
    let
        fun count (from: int) =
            if from = x
            then from :: []
            else from :: count (from + 1)
    in
        count 1
    end

Nel REPL SML:

val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list

La countup_from1funzione utilizza la chiusura dell'helper countche acquisisce e utilizza la variabile xdal suo contesto.

Nel secondo esempio, quando invoco una funzione create_multiplier t, torno indietro una funzione (in realtà, una chiusura) che moltiplica il suo argomento per t:

fun create_multiplier t = fn x => x * t

Nel REPL SML:

- fun create_multiplier t = fn x => x * t;
val create_multiplier = fn : int -> int -> int
- val m = create_multiplier 10;
val m = fn : int -> int
- m 4;
val it = 40 : int
- m 2;
val it = 20 : int

Quindi la variabile mè legata alla chiusura restituita dalla chiamata di funzione e ora posso usarla a piacimento.

Ora, affinché la chiusura funzioni correttamente per tutta la sua durata, dobbiamo estendere la durata della variabile acquisita t(nell'esempio è un numero intero ma potrebbe essere un valore di qualsiasi tipo). Per quanto ne so, in SML questo è reso possibile dalla garbage collection: la chiusura mantiene un riferimento al valore acquisito che viene successivamente eliminato dal garbage collector quando la chiusura viene distrutta.

La mia domanda: in generale, la raccolta dei rifiuti è l'unico meccanismo possibile per garantire che le chiusure siano sicure (richiamabili per tutta la loro vita)?

O quali altri meccanismi potrebbero garantire la validità delle chiusure senza garbage collection: copiare i valori acquisiti e archiviarli all'interno della chiusura? Limitare la durata della chiusura stessa in modo che non possa essere invocata dopo che le sue variabili acquisite sono scadute?

Quali sono gli approcci più popolari?

MODIFICARE

Non credo che l'esempio sopra possa essere spiegato / implementato copiando le variabili catturate nella chiusura. In generale, le variabili acquisite possono essere di qualsiasi tipo, ad esempio possono essere associate a un elenco molto grande (immutabile). Pertanto, nell'implementazione sarebbe molto inefficiente copiare questi valori.

Per completezza, ecco un altro esempio usando riferimenti (ed effetti collaterali):

(* Returns a closure containing a counter that is initialized
   to 0 and is incremented by 1 each time the closure is invoked. *)
fun create_counter () =
    let
        (* Create a reference to an integer: allocate the integer
           and let the variable c point to it. *)
        val c = ref 0
    in
        fn () => (c := !c + 1; !c)
    end

(* Create a closure that contains c and increments the value
   referenced by it it each time it is called. *)
val m = create_counter ();

Nel REPL SML:

val create_counter = fn : unit -> unit -> int
val m = fn : unit -> int
- m ();
val it = 1 : int
- m ();
val it = 2 : int
- m ();
val it = 3 : int

Pertanto, le variabili possono anche essere acquisite per riferimento e sono ancora attive dopo il completamento della chiamata di funzione che le ha create ( create_counter ()).


2
Tutte le variabili chiuse dovrebbero essere protette dalla garbage collection e tutte le variabili non chiuse dovrebbero essere idonee alla garbage collection. Ne consegue che qualsiasi meccanismo in grado di tracciare in modo affidabile se una variabile è chiusa o meno può anche recuperare in modo affidabile la memoria occupata dalla variabile.
Robert Harvey,

3
@btilly: refcounting è solo una delle tante strategie di implementazione diverse per un garbage collector. Non importa davvero come sia implementato il GC ai fini di questa domanda.
Jörg W Mittag

3
@btilly: cosa significa "vera" immondizia? Il refounting è solo un altro modo di implementare GC. La traccia è più popolare, probabilmente a causa delle difficoltà di raccogliere cicli con il conteggio. (Di solito, si ottiene comunque un GC di traccia separato, quindi perché preoccuparsi di implementare due GC se si riesce a cavarsela con uno.) Ma ci sono altri modi di gestire i cicli. 1) Basta vietarli. 2) Basta ignorarli. (Se stai eseguendo un'implementazione per script una tantum rapidi, perché no?) 3) Prova a rilevarli esplicitamente. (Si scopre che avere il refcount disponibile può velocizzarlo.)
Jörg W Mittag

1
Dipende dal perché vuoi chiusure in primo luogo. Se vuoi implementare, per esempio, una semantica di calcolo lambda completa, hai sicuramente bisogno di GC, punto. Non c'è altro modo per aggirare. Se vuoi qualcosa che assomigli in lontananza alle chiusure, ma non segua l'esatta semantica di tali (come in C ++, Delphi, qualunque cosa) - fai quello che vuoi, usa l'analisi della regione, usa la gestione della memoria completamente manuale.
Logica SK

2
@Mason Wheeler: le chiusure sono solo valori, in generale non è possibile prevedere come verranno spostate in fase di esecuzione. In questo senso, non sono niente di speciale, lo stesso sarebbe valido per una stringa, un elenco e così via.
Giorgio

Risposte:


14

Il linguaggio di programmazione Rust è interessante su questo aspetto.

Rust è un linguaggio di sistema, con un GC opzionale, ed è stato progettato con chiusure sin dall'inizio.

Come le altre variabili, le chiusure antiruggine sono disponibili in vari gusti. Le chiusure dello stack , le più comuni, sono utilizzabili con un solo colpo. Vivono in pila e possono fare riferimento a qualsiasi cosa. Le chiusure di proprietà diventano proprietarie delle variabili acquisite. Penso che vivano sul cosiddetto "mucchio di scambio", che è un mucchio globale. La loro durata dipende da chi li possiede. Le chiusure gestite vivono nell'heap locale dell'attività e vengono monitorate dal GC dell'attività. Non sono sicuro delle loro limitazioni di acquisizione, però.


1
Link molto interessante e riferimento al linguaggio Rust. Grazie. +1.
Giorgio

1
Ho pensato molto prima di accettare una risposta perché trovo che anche la risposta di Mason sia molto istruttiva. Ho scelto questo perché è sia informativo che cita una lingua meno conosciuta con un approccio originale alle chiusure.
Giorgio

Grazie per quello Sono molto entusiasta di questa lingua giovane e sono felice di condividere il mio interesse. Non sapevo se fossero possibili chiusure sicure senza GC, prima di sapere di Rust.
Barjak,

9

Sfortunatamente, iniziare con un GC ti rende vittima del syndrom XY:

  • le chiusure richiedono che le variabili chiuse siano attive fino a quando non lo fanno (per motivi di sicurezza)
  • usando il GC possiamo prolungare la durata di quelle variabili abbastanza a lungo
  • Syndrom XY: ci sono altri meccanismi per prolungare la durata?

Si noti, tuttavia, che l'idea di prolungare la durata di una variabile non è necessaria per una chiusura; è appena portato dal GC; la dichiarazione di sicurezza originale è solo che le variabili chiuse dovrebbero vivere finché la chiusura (e anche questo è traballante, potremmo dire che dovrebbero vivere fino a dopo l'ultima invocazione della chiusura).

Esistono essenzialmente due approcci che posso vedere (e potrebbero potenzialmente essere combinati):

  1. Estendi la durata delle variabili chiuse (come ad esempio un GC)
  2. Limita la durata della chiusura

Quest'ultimo è solo un approccio simmetrico. Non viene spesso utilizzato, ma se, come Rust, si dispone di un sistema di tipi compatibile con la regione, è certamente possibile.


7

La garbage collection non è necessaria per chiusure sicure, quando si acquisiscono variabili per valore. Un esempio importante è C ++. C ++ non ha una garbage collection standard. Le lambda in C ++ 11 sono chiusure (catturano variabili locali dall'ambito circostante). Ogni variabile catturata da una lambda può essere specificata per essere catturata per valore o per riferimento. Se viene acquisito per riferimento, puoi dire che non è sicuro. Tuttavia, se una variabile viene acquisita per valore, è sicura, poiché la copia acquisita e la variabile originale sono separate e hanno una durata indipendente.

Nell'esempio SML che hai fornito, è semplice da spiegare: le variabili sono catturate dal valore. Non è necessario "prolungare la durata" di alcuna variabile perché puoi semplicemente copiarne il valore nella chiusura. Ciò è possibile perché, in ML, non è possibile assegnare variabili. Quindi non c'è differenza tra una copia e molte copie indipendenti. Sebbene SML abbia la garbage collection, non è correlata alla cattura di variabili da parte delle chiusure.

La garbage collection non è inoltre necessaria per chiusure sicure durante l'acquisizione di variabili per riferimento (tipo di). Un esempio è l'estensione di Apple Blocks ai linguaggi C, C ++, Objective-C e Objective-C ++. Non esiste una garbage collection standard in C e C ++. I blocchi acquisiscono le variabili per valore per impostazione predefinita. Tuttavia, se viene dichiarata una variabile locale __block, i blocchi li acquisiscono apparentemente "per riferimento" e sono sicuri - possono essere utilizzati anche dopo l'ambito in cui è stato definito il blocco. Ciò che accade qui è che le __blockvariabili sono in realtà un struttura speciale sottostante e quando i blocchi vengono copiati (i blocchi devono essere copiati per poterli utilizzare all'esterno dell'ambito in primo luogo), "spostano" la struttura per il__block variabile nell'heap e il blocco gestisce la sua memoria, credo attraverso il conteggio dei riferimenti.


4
"La garbage collection non è necessaria per le chiusure": la domanda è se sia necessaria affinché la lingua possa applicare chiusure sicure. So che posso scrivere chiusure sicure in C ++ ma il linguaggio non le applica. Per chiusure che prolungano la durata delle variabili acquisite, vedere la modifica alla mia domanda.
Giorgio

1
Suppongo che la domanda potrebbe essere riformulata in: per chiusure sicure .
Matthieu M.

1
Il titolo contiene il termine "chiusure sicure", pensi che potrei formularlo in un modo migliore?
Giorgio

1
Potete per favore correggere il secondo paragrafo? In SML, le chiusure prolungano la durata dei dati a cui fanno riferimento le variabili acquisite. Inoltre, è vero che non è possibile assegnare variabili (modificarne l'associazione) ma si dispone di dati mutabili (tramite ref's). Quindi, OK, si può discutere se l'implementazione delle chiusure sia o meno correlata alla raccolta dei rifiuti, ma le affermazioni di cui sopra dovrebbero essere corrette.
Giorgio

1
@Giorgio: Che ne dici di adesso? Inoltre, in che senso trovi errata la mia affermazione secondo cui le chiusure non devono prolungare la durata di una variabile acquisita? Quando parli di dati mutabili, stai parlando di tipi di riferimento ( refs, array, ecc.) Che indicano una struttura. Ma il valore è il riferimento stesso, non la cosa a cui punta. Se ne hai var a = ref 1e ne fai una copia var b = ae la usi b, significa che stai ancora usando a? Hai accesso alla stessa struttura indicata da a? Sì. Questo è il modo in cui questi tipi funzionano in SML e non hanno nulla a che fare con le chiusure
user102008

6

La raccolta dei rifiuti non è necessaria per implementare le chiusure. Nel 2008, il linguaggio Delphi, che non è spazzatura, ha aggiunto un'implementazione di chiusure. Funziona così:

Il compilatore crea un oggetto funzione sotto il cofano che implementa un'interfaccia che rappresenta una chiusura. Tutte le variabili locali chiuse vengono modificate dai locali per la procedura di chiusura in campi sull'oggetto functor. Questo assicura che lo stato sia preservato per tutto il tempo in cui il funzione è.

La limitazione a questo sistema è che qualsiasi parametro passato in riferimento alla funzione che racchiude, così come il valore del risultato della funzione, non può essere catturato dal funzione perché non sono locali il cui ambito è limitato a quello della funzione di chiusura.

Al funzione di riferimento viene fatto riferimento il riferimento di chiusura, utilizzando lo zucchero sintattico per farlo sembrare allo sviluppatore come un puntatore a funzione anziché un'interfaccia. Utilizza il sistema di conteggio dei riferimenti di Delphi per le interfacce per garantire che l'oggetto functor (e tutto lo stato che detiene) rimanga "in vita" per tutto il tempo necessario, e quindi viene liberato quando il refcount scende a 0.


1
Ah, quindi è possibile solo catturare la variabile locale, non gli argomenti! Questo sembra un compromesso ragionevole e intelligente! +1
Giorgio

1
@Giorgio: può catturare argomenti, ma non quelli che sono parametri var .
Mason Wheeler

2
Perdi anche la possibilità di avere 2 chiusure che comunicano attraverso uno stato privato condiviso. Non lo troverai nei casi d'uso di base, ma limita la tua capacità di fare cose complesse. Ancora un ottimo esempio di ciò che è possibile!
btilly

3
@btilly: In realtà, se metti 2 chiusure all'interno della stessa funzione di chiusura, è perfettamente legale. Finiscono per condividere lo stesso oggetto funzione e, se modificano lo stesso stato l'uno dell'altro, i cambiamenti in uno si rifletteranno nell'altro.
Mason Wheeler

2
@MasonWheeler: "No. La garbage collection è di natura non deterministica; non esiste alcuna garanzia che un determinato oggetto venga mai raccolto, figuriamoci quando accadrà. Ma il conteggio dei riferimenti è deterministico: il compilatore garantisce che l'oggetto verrà liberato immediatamente dopo che il conteggio scende a 0. ". Se avessi un centesimo per ogni volta che avrei sentito quel mito perpetuato. OCaml ha un GC deterministico. Il thread sicuro C ++ shared_ptrnon è deterministico perché i distruttori corrono per diminuire a zero.
Jon Harrop,
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.