Hace poco asistí a un curso en línea sobre lenguajes de programación en el que, entre otros conceptos, se presentaron cierres. Escribo dos ejemplos inspirados en este curso para dar un contexto antes de hacer mi pregunta.
El primer ejemplo es una función SML que produce una lista de los números del 1 al 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 countup_from1
función usa el cierre auxiliar 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
Por lo tanto, la variable m
está vinculada al cierre devuelto por la llamada a la función y ahora puedo usarla 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 entero pero podría ser un valor de cualquier tipo). Hasta donde 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 se destruye el cierre.
Mi pregunta: en general, ¿es la recolección de basura el único mecanismo posible para garantizar que los cierres sean seguros (se pueden llamar 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? ¿Restringir la vida útil del cierre para que no se pueda invocar después de que las variables capturadas hayan expirado?
¿Cuáles son los enfoques más populares?
EDITAR
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 vinculadas a una lista muy grande (inmutable). Entonces, en la implementación sería muy ineficiente copiar estos valores.
En aras de la exhaustividad, aquí hay otro ejemplo que utiliza 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 que se create_counter ()
haya completado la llamada a la función que las creó ( ).