Fenner
A linguagem

Lambdas, eventos e async

Funções anônimas com captura, eventos de objeto e a chamada guardada, e async/await sobre TTask.

Funções anônimas#

Uma função ou procedimento escrito no meio de uma expressão, sem nome, que pode capturar as variáveis do ponto onde nasce. O tipo é reference to:

Fenner
type
  TBinary = reference to function(A, B: Int32): Int32;
  TAction = reference to procedure;
lambdas.fxp
program Lambdas;

type
  TBinary = reference to function(A, B: Int32): Int32;
  TAction = reference to procedure;

function Reduce(const AValues: array of Int32; AFunc: TBinary): Int32;
var
  I: Int32;
begin
  Result := AValues[0];
  for I := 1 to High(AValues) do
    Result := AFunc(Result, AValues[I]);
end function;

procedure Demo;
var
  Counter: Int32;
  Bump: TAction;
begin
  var Sum := function(A, B: Int32): Int32;
    begin
      Exit(A + B);
    end;
  WriteLn('Sum: ', Sum(2, 3));

  // corpo curto com =>
  var Product := function(A, B: Int32): Int32; => Exit(A * B);
  WriteLn('Reduce soma:    ', Reduce([1, 2, 3, 4], Sum));
  WriteLn('Reduce produto: ', Reduce([1, 2, 3, 4], Product));

  // captura por referência: o fecho enxerga a variável do dono
  Counter := 0;
  Bump := procedure;
    begin
      Inc(Counter);
    end;
  Bump();
  Bump();
  Bump();
  WriteLn('Counter: ', Counter);
end procedure;

begin
  Demo;
end.
Saída
Sum: 5
Reduce soma:    10
Reduce produto: 24
Counter: 3
  • var F := function(…)…: o tipo é inferido, e F(2, 3) chama.
  • Corpo-seta: function(A, B: Int32): Int32; => Exit(A * B) é a outra grafia do mesmo fecho.
  • Captura por referência: a variável local que um fecho toca deixa de morar no frame e passa a viver numa caixa no heap (um vetor dinâmico de um elemento). Os dois lados falam com a mesma caixa: o que a lambda escreve, o dono enxerga. Duas lambdas sobre a mesma variável usam uma caixa só.
  • O fecho pode viver mais que a rotina que o criou: como a caixa tem contagem de referências, nada fica pendurado.
  • Um fecho serve em qualquer lugar onde um reference to com a mesma assinatura é pedido (Reduce([1, 2, 3, 4], Sum)).

Eventos#

Um evento é um tipo procedural of object: um método com o objeto a que pertence.

Fenner
type
  TNotifyEvent = procedure(const ASender: TObject) of object;

  TButton = class(TObject)
  private
    FOnClick: TNotifyEvent;
  public
    property OnClick: TNotifyEvent read FOnClick write FOnClick;
  end class;

A chamada guardada#

Evento!(args) chama o evento só se houver método ligado. Sem ele, nada acontece: nem a chamada, nem a avaliação dos argumentos. É o if Assigned(F) then F(...) do Delphi numa expressão só.

events.fxp
program Events;

type
  TNotifyEvent = procedure(const ASender: TObject) of object;

  TButton = class(TObject)
  private
    FOnClick: TNotifyEvent;
  public
    property OnClick: TNotifyEvent read FOnClick write FOnClick;
    procedure Click;
  end class;

  TListener = class(TObject)
  public
    procedure Pressed(const ASender: TObject);
  end class;

procedure TButton.Click;
begin
  // chamada guardada: sem método ligado, nada acontece (nem os argumentos são avaliados)
  FOnClick!(Self);
end procedure;

procedure TListener.Pressed(const ASender: TObject);
begin
  WriteLn('  botão clicado');
end procedure;

var
  Button: TButton;
  Listener: TListener;
begin
  Button := TButton.Create;
  Listener := TListener.Create;

  WriteLn('sem tratador:');
  Button.Click;

  Button.OnClick := Listener.Pressed;
  WriteLn('com tratador:');
  Button.Click;
end.
Saída
sem tratador:
com tratador:
  botão clicado

O evento que se desliga sozinho#

Delegate(T) resolve o problema do evento cujo alvo morre: veja Memória.

async e await#

async marca uma função ou um procedimento que roda numa task. A função devolve um TTask<T> (e o procedimento, um TTaskNoReturn); await espera pelo resultado.

core-async.fxp
program AsyncDemo;

type
  TWorker = class(TObject)
  public
    async function Square(const N: Int32): Int32;
    async function Fail(const N: Int32): Int32;
    async procedure Hello;
  end class;

async function TWorker.Square(const N: Int32): Int32;
begin
  Exit(N * N);
end function;

async function TWorker.Fail(const N: Int32): Int32;
begin
  raise Exception.Create('falhou com ' + IntToUStr(N));
end function;

async procedure TWorker.Hello;
begin
  WriteLn('olá de dentro de uma task');
end procedure;

var
  Worker: TWorker;
  A, B, C, D: TTask<Int32>;
  Done: TTaskNoReturn;
  R1, R2: Int32;
begin
  Worker := TWorker.Create;

  // a task já começa a correr quando é criada (hot start)
  A := Worker.Square(6);
  B := Worker.Square(7);
  WriteLn('resultado: ', await A, ' e ', await B);

  Done := Worker.Hello;
  await Done;

  // esperar várias de uma vez: o fio dorme em todas ao mesmo tempo
  C := Worker.Square(8);
  D := Worker.Square(9);
  R1, R2 := await C, D;
  WriteLn('várias: ', R1, ' e ', R2);

  // a exceção fica guardada na task e o await a dispara de novo
  try
    WriteLn(await Worker.Fail(3));
  except
    on E: Exception do
      WriteLn('capturada no await: ', E.Message);
  end try;
end.
Saída
resultado: 36 e 49
olá de dentro de uma task
várias: 64 e 81
capturada no await: falhou com 3
  • As duas grafias dão no mesmo: async function Venda: UString equivale a uma função que devolve TTask<UString> criada com uma lambda.
  • A task já começa a correr quando é criada (hot start). Várias tasks no ar correm ao mesmo tempo, e o await só espera.
  • R1, R2 := await C, D; espera várias de uma vez: o ganho é de espera, não de paralelismo, porque as tasks já estão correndo; o fio dorme em todos os eventos ao mesmo tempo, em vez de sondar um a um. O _ descarta o resultado (é o que recebe a task de um async procedure).
  • A exceção fica guardada na task e o await a dispara de novo, no ponto de quem espera.
  • Um await dentro de uma task não enforca o pool de threads.
  • Uma task criada e nunca esperada é liberada sem problema.