Fenner
A linguagem

Closures em detalhe

O que uma função anônima captura, por cópia ou por caixa, o caso do var/out, noescape e o objeto estático das closures sem captura.

O que é e por que existe#

A página de lambdas mostra a sintaxe. Esta explica o que acontece com as variáveis que uma função anônima usa: quem é dono delas, até quando vivem e o que é proibido. Um reference to guarda a função junto com o que ela capturou.

Fenner
type
  TAction = reference to procedure;
  TTransform = reference to function(const AValue: Int32): Int32;

function MakeCounter(out AGet: TTransform): TAction;
var
  LCount: Int32;
begin
  LCount := 0;
  AGet := function(const AValue: Int32): Int32; begin Result := LCount + AValue; end;
  Result := procedure; begin Inc(LCount); end;
end function;

O contador e o leitor enxergam a mesma LCount, que continua viva depois que MakeCounter retorna.

rc-closures.fxp
program Closures;
// Demo: funções anônimas e closures. `reference to` é o tipo "referência para rotina"
// que aceita tanto uma rotina comum quanto uma função anônima que CAPTURA variáveis.

uses System.Remainder;

type
  TAction = reference to procedure;
  TTransform = reference to function(const AValue: Int32): Int32;

// A variável local LCount é capturada: o contador (Result) e o leitor (AGet) enxergam
// a MESMA variável, que continua viva depois que MakeCounter retorna.
function MakeCounter(out AGet: TTransform): TAction;
var
  LCount: Int32;
begin
  LCount := 0;
  AGet := function(const AValue: Int32): Int32; begin Result := LCount + AValue; end;
  Result := procedure; begin Inc(LCount); end;
end function;

// Uma "fábrica de funções": cada chamada captura o seu próprio AFactor.
function MakeMultiplier(AFactor: Int32): TTransform;
begin
  Result := function(const AValue: Int32): Int32; begin Result := AValue * AFactor; end;
end function;

// Parâmetro `const` gerenciado (cadeia) também é capturado com segurança.
function MakeGreeter(const AName: UString): TAction;
begin
  Result := procedure; begin WriteLn('Olá, ', AName, '!'); end;
end function;

// Função de ordem superior: recebe a função a aplicar.
function ApplyTwice(AFunc: TTransform; AValue: Int32): Int32;
begin
  Result := AFunc(AFunc(AValue));
end function;

var
  LTick: TAction;
  LPeek, LTriple: TTransform;
  LGreet: TAction;
begin
  LTick := MakeCounter(LPeek);
  LTick();
  LTick();
  LTick();
  WriteLn('contador depois de 3 ticks (+10): ', LPeek(10));

  LTriple := MakeMultiplier(3);
  WriteLn('triplo de 7: ', LTriple(7), '; duas vezes: ', ApplyTwice(LTriple, 7));

  // função anônima passada direto, sem nome
  WriteLn('quadrado aplicado duas vezes em 3: ',
          ApplyTwice(function(const V: Int32): Int32; begin Result := V * V; end, 3));

  LGreet := MakeGreeter('Ana');
  LGreet();
end.
Saída
contador depois de 3 ticks (+10): 13
triplo de 7: 21; duas vezes: 63
quadrado aplicado duas vezes em 3: 81
Olá, Ana!

Demo 45. Contador compartilhado, fábrica de multiplicadores, captura de const de cadeia e função de ordem superior.

Como cada coisa é capturada#

O que a closure usaComo é capturado
variável localvai para uma caixa compartilhada: a rotina e a closure veem a mesma variável, que vive enquanto alguma closure a segurar
parâmetro por valor ou constcópia, com contagem de referências: a closure guarda a sua referência e a solta ao morrer
const de tipo gerenciado (cadeia, vetor, record com cadeia, classe, interface)cópia segura; capturar vetor e record por const já funcionou errado e foi corrigido
parâmetro var/outsó se a closure não puder sobreviver à rotina (veja abaixo)

Uma closure que guarda objetos os mantém vivos (ARC). Um ciclo objeto ↔ closure precisa de weak.

Parâmetros var/out e noescape#

Copiar um var para dentro da closure seria uma cópia silenciosa que nunca chegaria ao chamador, então o compilador recusa o caso perigoso: a closure que pode sobreviver à rotina dona do parâmetro. Retornar a closure, por exemplo:

Fenner
function MakeShow(var X: Int32): TRefProcedure;
begin
  Result := procedure; begin WriteLn('X=', X); end;    // FE1457
end function;

O erro é FE1457: a closure só pode ser passada direto como argumento de uma rotina que apenas a chama (ou a repassa a parâmetros assim), ou cujo parâmetro seja declarado noescape; ou copie o parâmetro para uma variável local. Já a closure que não escapa funciona: atribuída a uma local e chamada dentro da própria rotina, ou passada a uma rotina que só a chama, ela altera o var de verdade (verificado: Inc(AX) dentro da closure muda a variável do chamador).

noescape é a promessa de uma rotina de não guardar nem repassar a closure recebida:

Fenner
procedure CallIt(noescape AAction: TAction);   // "const noescape" também vale
begin
  AAction();                                   // só chamar, testar com Assigned ou repassar a outro noescape
end procedure;

O compilador confere a promessa quando vê o corpo (FE1456 se o corpo a guarda) e confia nela quando não vê (método virtual, interface, módulo já compilado). FE1455: noescape só vale em parâmetro de valor ou const do tipo reference to.

Closure sem captura: objeto estático#

Uma função anônima que não captura nada (nem local, nem parâmetro, nem Self) e a conversão de uma rotina livre para reference to não alocam: todas as avaliações do mesmo ponto do fonte devolvem o mesmo objeto estático (ClosureStatic, ligado por padrão). É cerca de 5 vezes mais rápido e fica fora do relatório de vazamentos. A consequência visível é na identidade: duas avaliações da mesma lambda sem captura são iguais (Mk = Mk é True). Para o comportamento antigo: {$config ClosureStatic off}.

Veja também#