¿Se necesita la recolección de basura para implementar cierres seguros?

14

Hace poco asistí a un curso en línea sobre lenguajes de programación en el que, entre otros conceptos, se presentaron los cierres. Escribo dos ejemplos inspirados en este curso para dar un poco de contexto antes de hacer mi pregunta.

El primer ejemplo es una función SML que produce una lista de los números de 1 a x, donde x es el parámetro de la función:

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

En el SML REPL:

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

La función countup_from1 usa el cierre de ayuda count que captura y usa la variable x de su contexto.

En el segundo ejemplo, cuando invoco una función create_multiplier t , recupero una función (en realidad, un cierre) que multiplica su argumento por t:

fun create_multiplier t = fn x => x * t

En el 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

Entonces, la variable m está vinculada al cierre devuelto por la llamada a la función y ahora puedo usarlo a voluntad.

Ahora, para que el cierre funcione correctamente durante toda su vida útil, necesitamos extender la vida útil de la variable capturada t (en el ejemplo, es un número entero pero podría ser un valor de cualquier tipo). Por lo que sé, en SML esto es posible gracias a la recolección de basura: el cierre mantiene una referencia al valor capturado, que luego es eliminado por el recolector de basura cuando El cierre se destruye.

Mi pregunta: en general, es la recolección de basura el único mecanismo posible para ¿Asegurarse de que los cierres sean seguros (que se puedan reclamar durante toda su vida útil)?

O, ¿cuáles son otros mecanismos que podrían garantizar la validez de los cierres sin recolección de basura: copiar los valores capturados y almacenarlos dentro del cierre? ¿Restrinja la vida útil del cierre para que no se pueda invocar después de que hayan expirado las variables capturadas?

¿Cuáles son los enfoques más populares?

EDIT

No creo que el ejemplo anterior se pueda explicar / implementar copiando las variables capturadas en el cierre. En general, las variables capturadas pueden ser de cualquier tipo, por ejemplo pueden estar vinculados a una lista muy grande (inmutable). Por lo tanto, en la implementación sería muy ineficiente copiar estos valores.

En aras de la integridad, aquí hay otro ejemplo que usa referencias (y efectos secundarios):

(* 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 ();

En el 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

Por lo tanto, las variables también se pueden capturar por referencia y siguen vivas después de la llamada a la función que los creó ( create_counter () ) se ha completado.

    
pregunta Giorgio 09.03.2013 - 01:00
fuente

4 respuestas

14

El lenguaje de programación Rust es interesante en este aspecto.

Rust es un lenguaje de sistema, con un GC opcional, y fue diseñado con cierres de el principio.

Como las otras variables, los cierres de óxido vienen en varios sabores. Los cierres de pila , los más comunes, son para uso de una sola vez. Viven en la pila y pueden hacer referencia a cualquier cosa. Cierres de propiedad se apropian de las variables capturadas. Creo que viven en el llamado "intercambio de pila", que es un montón global. Su vida útil depende de quién los posee. Los cierres gestionados se encuentran en el montón local de la tarea y el GC de la tarea los rastrea. Sin embargo, no estoy seguro de sus limitaciones de captura.

    
respondido por el barjak 11.03.2013 - 00:30
fuente
9

Desafortunadamente, al comenzar con un GC, eres una víctima del síndrome XY:

  • los cierres requieren que las variables sobre las que se cerraron en vivo, siempre que el cierre (por razones de seguridad)
  • utilizando el GC podemos extender la vida útil de esas variables el tiempo suficiente
  • Síndrom XY: ¿existen otros mecanismos para extender la vida útil?

Sin embargo, tenga en cuenta que la idea de extender el tiempo de vida de una variable no es necesaria para un cierre; acaba de ser traído por el GC; la declaración de seguridad original es simplemente las variables sobre cerradas deben vivir tanto como el cierre (e incluso eso es inestable, podríamos decir que deberían vivir hasta después de la última invocación del cierre).

Hay, esencialmente, dos enfoques que puedo ver (y potencialmente podrían combinarse):

  1. Extienda la vida útil de las variables cerradas (como hace un GC, por ejemplo)
  2. Restrinja la vida útil del cierre

Este último es solo un enfoque simétrico. No se usa a menudo, pero si, como Rust, tiene un sistema de tipo que tiene en cuenta la región, es ciertamente posible.

    
respondido por el Matthieu M. 09.03.2013 - 15:45
fuente
7

La recolección de basura no es necesaria para los cierres seguros, al capturar variables por valor. Un ejemplo prominente es C ++. C ++ no tiene una recolección de basura estándar. Lambdas en C ++ 11 son cierres (capturan variables locales del ámbito circundante). Cada variable capturada por un lambda puede especificarse para ser capturada por valor o por referencia. Si se captura por referencia, entonces puede decir que no es seguro. Sin embargo, si una variable se captura por valor, entonces es segura, porque la copia capturada y la variable original están separadas y tienen tiempos de vida independientes.

En el ejemplo de SML que dio, es fácil de explicar: las variables se capturan por valor. No hay necesidad de "extender la vida útil" de ninguna variable porque simplemente puede copiar su valor en el cierre. Esto es posible porque, en ML, las variables no pueden asignarse. Así que no hay diferencia entre una copia y muchas copias independientes. Aunque SML tiene recolección de basura, no está relacionado con la captura de variables mediante cierres.

La recolección de basura tampoco es necesaria para cierres seguros al capturar variables por referencia (tipo de). Un ejemplo es la extensión Apple Blocks para los lenguajes C, C ++, Objective-C y Objective-C ++. No hay recolección de basura estándar en C y C ++. Bloques de captura de variables por valor por defecto. Sin embargo, si una variable local se declara con __block , entonces los bloques los capturan aparentemente "por referencia" y son seguros: se pueden usar incluso después del alcance en el que se definió el bloque. Lo que sucede aquí es que% Las variables __block son en realidad una estructura especial debajo, y cuando los bloques se copian (los bloques deben copiarse para poder usarlos fuera del ámbito en primer lugar), "mueven" la estructura para la variable __block al montón, y el bloque gestiona su memoria, creo que a través del conteo de referencias.

    
respondido por el user102008 09.03.2013 - 09:22
fuente
6

La recolección de basura no es necesaria para implementar cierres. En 2008, el lenguaje Delphi, que no se recolecta como basura, agregó una implementación de cierres. Funciona así:

El compilador crea un objeto functor bajo el capó que implementa una interfaz que representa un cierre. Todas las variables locales cerradas se cambian de locales para el procedimiento de cierre a campos en el objeto functor. Esto garantiza que el estado se mantenga durante todo el tiempo que esté el functor.

La limitación de este sistema es que cualquier parámetro pasado por referencia a la función de envolvente, así como el valor de resultado de la función, no puede ser capturado por el functor porque no son locales cuyo alcance está limitado al de la función de envolvente.

Se hace referencia al funtor mediante la referencia de cierre, que utiliza azúcar sintáctica para hacer que el desarrollador se vea como un puntero de función en lugar de una interfaz. Utiliza el sistema de conteo de referencias de Delphi para las interfaces para garantizar que el objeto funtor (y todo el estado que posee) permanezca "vivo" todo el tiempo que sea necesario, y luego se libera cuando el refcount cae a 0.

    
respondido por el Mason Wheeler 09.03.2013 - 01:25
fuente

Lea otras preguntas en las etiquetas