Recentemente, participei de um curso on-line sobre linguagens de programação, no qual, entre outros conceitos, foram apresentados encerramentos. Escrevo dois exemplos inspirados neste curso para dar algum contexto antes de fazer minha pergunta.
O primeiro exemplo é uma função SML que produz uma lista dos números de 1 a x, em que x é o parâmetro da função:
fun countup_from1 (x: int) =
let
fun count (from: int) =
if from = x
then from :: []
else from :: count (from + 1)
in
count 1
end
No SML REPL:
val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list
A countup_from1
função usa o fechamento auxiliar count
que captura e usa a variável x
de seu contexto.
No segundo exemplo, quando invoco uma função create_multiplier t
, recebo de volta uma função (na verdade, um fechamento) que multiplica seu argumento por t:
fun create_multiplier t = fn x => x * t
No 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
Portanto, a variável m
está vinculada ao fechamento retornado pela chamada de função e agora posso usá-la à vontade.
Agora, para que o fechamento funcione adequadamente durante toda a sua vida útil, precisamos estender a vida útil da variável capturada t
(no exemplo, é um número inteiro, mas pode ser um valor de qualquer tipo). Tanto quanto eu sei, no SML isso é possível pela coleta de lixo: o fechamento mantém uma referência ao valor capturado que é posteriormente descartado pelo coletor de lixo quando o fechamento é destruído.
Minha pergunta: em geral, a coleta de lixo é o único mecanismo possível para garantir que os fechamentos sejam seguros (exigíveis durante toda a vida útil)?
Ou quais são outros mecanismos que poderiam garantir a validade dos fechamentos sem coleta de lixo: copie os valores capturados e armazene-os dentro do fechamento? Restringir o tempo de vida do próprio fechamento para que ele não possa ser chamado depois que suas variáveis capturadas expirarem?
Quais são as abordagens mais populares?
EDITAR
Eu não acho que o exemplo acima possa ser explicado / implementado copiando as variáveis capturadas no fechamento. Em geral, as variáveis capturadas podem ser de qualquer tipo, por exemplo, podem ser vinculadas a uma lista muito grande (imutável). Portanto, na implementação, seria muito ineficiente copiar esses valores.
Por uma questão de exaustividade, aqui está outro exemplo usando referências (e efeitos colaterais):
(* 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 ();
No 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
Portanto, as variáveis também podem ser capturadas por referência e ainda estão ativas após a conclusão da chamada de função que as criou ( create_counter ()
).
Respostas:
A linguagem de programação Rust é interessante nesse aspecto.
Rust é uma linguagem do sistema, com um GC opcional, e foi projetada com fechamentos desde o início.
Como as outras variáveis, os fechamentos de ferrugem têm vários sabores. Os fechamentos de pilha , os mais comuns, são para uso único. Eles vivem na pilha e podem fazer referência a qualquer coisa. Os fechamentos proprietários assumem a propriedade das variáveis capturadas. Eu acho que eles vivem na chamada "pilha de troca", que é uma pilha global. Sua vida útil depende de quem os possui. Os fechamentos gerenciados ficam no heap local da tarefa e são rastreados pelo GC da tarefa. Eu não tenho certeza sobre suas limitações de captura, no entanto.
fonte
Infelizmente, começar com um GC faz de você uma vítima da síndrome XY:
Observe, no entanto, que a idéia de prolongar a vida útil de uma variável não é necessária para um fechamento; é apenas trazido pelo GC; a declaração de segurança original é que as variáveis fechadas devem permanecer enquanto o fechamento (e mesmo que seja instável, poderíamos dizer que elas deveriam permanecer até depois da última invocação do fechamento).
Existem, essencialmente, duas abordagens que posso ver (e elas podem ser combinadas):
O último é apenas uma abordagem simétrica. Geralmente não é usado, mas se, como o Rust, você possui um sistema de tipo com reconhecimento de região, é certamente possível.
fonte
A coleta de lixo não é necessária para fechamentos seguros ao capturar variáveis por valor. Um exemplo proeminente é o C ++. C ++ não tem coleta de lixo padrão. Lambdas no C ++ 11 são encerramentos (eles capturam variáveis locais do escopo circundante). Cada variável capturada por um lambda pode ser especificada para ser capturada por valor ou por referência. Se for capturado por referência, você poderá dizer que não é seguro. No entanto, se uma variável é capturada por valor, ela é segura, porque a cópia capturada e a variável original são separadas e têm vida útil independente.
No exemplo SML que você deu, é simples de explicar: as variáveis são capturadas por valor. Não há necessidade de "estender a vida útil" de qualquer variável, pois você pode apenas copiar seu valor no fechamento. Isso é possível porque, no ML, as variáveis não podem ser atribuídas. Portanto, não há diferença entre uma cópia e muitas cópias independentes. Embora o SML tenha coleta de lixo, ele não está relacionado à captura de variáveis por fechamento.
A coleta de lixo também não é necessária para fechamentos seguros ao capturar variáveis por referência (tipo de). Um exemplo é a extensão Apple Blocks para as linguagens C, C ++, Objective-C e Objective-C ++. Não há coleta de lixo padrão em C e C ++. Os blocos capturam variáveis por valor por padrão. No entanto, se uma variável local é declarada com
__block
, os blocos as capturam aparentemente "por referência" e são seguras - elas podem ser usadas mesmo após o escopo em que o bloco foi definido. O que acontece aqui é que as__block
variáveis são realmente uma estrutura especial abaixo, e quando os blocos são copiados (os blocos devem ser copiados para usá-los fora do escopo, em primeiro lugar), eles "movem" a estrutura para o__block
variável na pilha, e o bloco gerencia sua memória, acredito através da contagem de referência.fonte
ref
de). Então, OK, pode-se discutir se a implementação de fechamentos está relacionada à coleta de lixo ou não, mas as declarações acima devem ser corrigidas.ref
s, matrizes, etc.) que apontam para uma estrutura. Mas o valor é a referência em si, não a coisa para a qual aponta. Se você possuivar a = ref 1
e faz uma cópiavar b = a
e usab
, isso significa que você ainda está usandoa
? Você tem acesso à mesma estrutura apontada pora
? Sim. Isso é apenas como esses tipos trabalhar em SML e não têm nada a ver com fechamentosA coleta de lixo não é necessária para implementar fechamentos. Em 2008, a linguagem Delphi, que não é coletada como lixo, adicionou uma implementação de fechamentos. Funciona assim:
O compilador cria um objeto functor sob o capô que implementa uma interface que representa um fechamento. Todas as variáveis locais fechadas são alteradas de locais para o procedimento de fechamento para campos no objeto functor. Isso garante que o estado seja preservado enquanto o functor estiver.
A limitação deste sistema é que qualquer parâmetro passado por referência à função envolvente, bem como o valor resultante da função, não pode ser capturado pelo functor porque eles não são locais cujo escopo é limitado ao da função envolvente.
O functor é referido pela referência de fechamento, usando açúcar sintático para fazer com que pareça ao desenvolvedor como um ponteiro de função em vez de uma Interface. Ele usa o sistema de contagem de referência do Delphi para interfaces para garantir que o objeto functor (e todo o estado que ele possui) permaneça "vivo" o tempo que for necessário, e então será liberado quando o refcount cair para 0.
fonte
shared_ptr
não é determinístico porque os destruidores correm para diminuir para zero.