La garbage collection è necessaria per implementare chiusure sicure?

14

Recentemente ho partecipato a un corso online sui linguaggi di programmazione in cui, tra gli altri concetti, sono state presentate le chiusure. Annoto due esempi ispirati a questo corso per dare qualche 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

In SML REPL:

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

La funzione countup_from1 utilizza la chiusura helper count che cattura e utilizza la variabile x dal suo contesto.

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

fun create_multiplier t = fn x => x * t

In SML REPL:

- 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 alla funzione e ora posso usarla a volontà.

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

La mia domanda: in generale, la garbage collection è l'unico meccanismo possibile per assicurarsi che le chiusure siano sicure (richiamabili durante tutta la loro vita)?

O quali sono altri meccanismi che potrebbero garantire la validità delle chiusure senza garbage collection: copiare i valori catturati e memorizzarli all'interno della chiusura? Limita la durata della chiusura stessa in modo che non possa essere invocato dopo che le sue variabili catturate sono scadute?

Quali sono gli approcci più popolari?

Modifica

Non penso 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 es. possono essere associati a una lista molto grande (immutabile). Quindi, nell'implementazione sarebbe molto inefficiente copiarli valori.

Per completezza, ecco un altro esempio che usa i riferimenti (e 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 ();

In SML REPL:

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

Quindi, le variabili possono anche essere catturate per riferimento e sono ancora vivi dopo la chiamata alla funzione che li ha creati ( create_counter () ) è stata completata.

    
posta Giorgio 09.03.2013 - 01:00
fonte

4 risposte

14

Il linguaggio di programmazione di Rust è interessante su questo aspetto.

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

Come le altre variabili, le chiusure di ruggine sono disponibili in vari gusti. Le chiusure dello stack , le più comuni, sono per l'utilizzo one-shot. Vivono in pila e possono fare riferimento a qualsiasi cosa. Chiusure di proprietà acquisiscono la proprietà delle variabili acquisite. Penso che vivano sul cosiddetto "heap di scambio", che è un ammasso globale. La loro durata dipende da chi li possiede. Chiusure gestite vivono nell'heap locale delle attività e vengono tracciate dal GC dell'attività. Non sono sicuro dei loro limiti di cattura, però.

    
risposta data 11.03.2013 - 00:30
fonte
9

Sfortunatamente iniziare con un GC ti rende vittima della sindrome XY:

  • le chiusure richiedono che le variabili chiuse sopra vivano fino a quando la chiusura fa (per ragioni di sicurezza)
  • utilizzando il GC possiamo prolungare la durata di tali variabili abbastanza a lungo
  • Sindrome XY: esistono altri meccanismi per estendere la durata?

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

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

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

Quest'ultimo è solo un approccio simmetrico. Non è spesso usato, ma se, come Rust, hai un sistema di tipo region-aware, allora è certamente possibile.

    
risposta data 09.03.2013 - 15:45
fonte
7

La raccolta dei dati inutili non è necessaria per chiusure sicure, quando si acquisiscono le variabili in base al valore. Un esempio importante è il C ++. C ++ non ha una garbage collection standard. Lambdas 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 catturato per riferimento, allora puoi dire che non è sicuro. Tuttavia, se una variabile viene catturata dal valore, allora è sicura, perché la copia acquisita e la variabile originale sono separate e hanno una durata indipendente.

Nell'esempio SML che hai fornito, è semplice spiegare: le variabili sono catturate per valore. Non è necessario "estendere la durata" di qualsiasi variabile perché è sufficiente copiarne il valore nella chiusura. Questo è possibile perché, in ML, le variabili non possono essere assegnate a. Quindi non c'è differenza tra una copia e molte copie indipendenti. Sebbene SML abbia una garbage collection, non è correlato alla cattura di variabili da parte delle chiusure.

Anche la raccolta dei dati inutili non è necessaria per chiusure sicure quando si acquisiscono 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 in base al valore per impostazione predefinita. Tuttavia, se una variabile locale è dichiarata con __block , i blocchi li catturano apparentemente "per riferimento" e sono sicuri - possono essere utilizzati anche dopo l'ambito in cui è stato definito il blocco. Quello che succede qui è che% le variabili di__block sono in realtà una speciale struttura al di sotto e quando i blocchi vengono copiati (i blocchi devono essere copiati per poterli utilizzare al di fuori dell'ambito in primo luogo), "spostano" la struttura per la variabile __block nell'heap, e il blocco gestisce la sua memoria, credo attraverso il conteggio dei riferimenti.

    
risposta data 09.03.2013 - 09:22
fonte
6

La raccolta dei dati inutili non è necessaria per implementare le chiusure. Nel 2008, la lingua Delphi, che non è raccolta dalla spazzatura, ha aggiunto un'implementazione delle chiusure. Funziona così:

Il compilatore crea un oggetto functor sotto la cappa che implementa un'interfaccia che rappresenta una chiusura. Tutte le variabili locali chiuse vengono cambiate dai locali per la procedura di inclusione ai campi dell'oggetto functor. Ciò garantisce che lo stato venga conservato fino a quando il funtore è.

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

Il functor è definito dal riferimento di chiusura, usando lo zucchero sintattico per farlo sembrare allo sviluppatore come un puntatore a funzione invece di un'interfaccia. Utilizza il sistema di conteggio di riferimento di Delphi per le interfacce per garantire che l'oggetto funtore (e tutto lo stato che detiene) rimanga "vivo" finché necessario, e quindi viene liberato quando il conto scende a 0.

    
risposta data 09.03.2013 - 01:25
fonte

Leggi altre domande sui tag