Fenner
A linguagem

Iteradores e corrotinas

iterator function com yield (máquina de estados) e TGenerator de Fenner.Coroutines (pilha própria): sequências sob demanda.

O que é e por que existe#

Um gerador produz valores um a um, sob demanda: o laço pede o próximo, o gerador roda até entregar um valor e fica suspenso. Isso permite sequências enormes ou infinitas sem guardar nada em memória, e dispensa escrever à mão um enumerador com estado (for..in e enumeradores).

O Fenner oferece dois mecanismos, com a diferença importante de onde o yield pode ficar.

iterator functionTGenerator<T> (Fenner.Coroutines)
Mecanismomáquina de estados gerada pelo compiladorcorrotina com pilha própria (uma fiber do Windows)
Onde pode suspendersó no corpo da própria funçãode qualquer profundidade de chamada
Recursãoprecisa de yield fromuma chamada comum a Yield em rotina recursiva
BibliotecaFenner.IteratorsFenner.Coroutines
Plataformanão depende de fibers do sistemasó Windows 64 bits

Ambos moram em Compiler\extras; o .config do projeto o põe no SourcePath.

iterator function e yield#

Fenner
iterator function TGen.Squares(ACount: Int32): Int32;
var
  I: Int32;
begin
  for I := 1 to ACount do
    yield I * I;                  // entrega um valor e suspende aqui
end function;

for X in LGen.Squares(5) do Write(' ', X);     // 1 4 9 16 25

O estado local (como o X do Collatz) sobrevive entre um yield e outro. yield from OutroGerador delega a outro gerador: é o que permite o percurso recursivo de uma árvore. Também se usa à mão: TIterator<Int32> com MoveNext/Current, e quem o cria à mão o libera com Free. Parar cedo é barato: o resto da sequência nem existe.

Iterators.fxp
program Iterators;
// Demo: geradores com `iterator function` + `yield` (máquina de estados, sem pilha própria).
// Cada `yield` entrega um valor ao laço `for ... in` e suspende a função até o próximo passo.
// Os valores são produzidos SOB DEMANDA: dá para ter sequências enormes (ou infinitas) sem memória.

uses System.Remainder, Fenner.Iterators;   // o Iterators.config põe Compiler\extras no SourcePath

type
  TNode = class(TObject)
  public
    Value: Int32;
    Left, Right: TNode;
    constructor Create(const AValue: Int32);
    destructor Destroy; override;
  end class;

  TGen = class(TObject)
  public
    iterator function Squares(ACount: Int32): Int32;
    iterator function Collatz(AStart: Int32): Int32;
    iterator function InOrder(const ANode: TNode): Int32;
  end class;

constructor TNode.Create(const AValue: Int32);
begin
  inherited Create;
  Value := AValue;
end constructor;

destructor TNode.Destroy;
begin
  Left.Free;
  Right.Free;
  inherited Destroy;
end destructor;

function Insert(const ARoot: TNode; const AValue: Int32): TNode;
begin
  if ARoot = nil then
    Exit(TNode.Create(AValue));
  if AValue < ARoot.Value then
    ARoot.Left := Insert(ARoot.Left, AValue)
  else
    ARoot.Right := Insert(ARoot.Right, AValue);
  Result := ARoot;
end function;

iterator function TGen.Squares(ACount: Int32): Int32;
var
  I: Int32;
begin
  for I := 1 to ACount do
    yield I * I;                  // entrega um valor e suspende aqui
end function;

// A sequência de Collatz: o laço guarda seu estado entre um yield e outro.
iterator function TGen.Collatz(AStart: Int32): Int32;
var
  X: Int32;
begin
  X := AStart;
  repeat
    yield X;
    if (X mod 2) = 0 then X := X div 2 else X := 3 * X + 1;
  until X = 1;
  yield 1;
end function;

// `yield from` delega para outro gerador: percurso em ordem de uma árvore, recursivo.
iterator function TGen.InOrder(const ANode: TNode): Int32;
begin
  if ANode = nil then
    Exit;
  yield from InOrder(ANode.Left);
  yield ANode.Value;
  yield from InOrder(ANode.Right);
end function;

var
  LGen: TGen;
  LRoot: TNode;
  LIter: TIterator<Int32>;
  LValues: array of Int32;
  X, LTotal: Int32;
begin
  LGen := TGen.Create;

  Write('quadrados:');
  for X in LGen.Squares(5) do Write(' ', X);
  WriteLn;

  Write('Collatz de 6:');
  for X in LGen.Collatz(6) do Write(' ', X);
  WriteLn;

  LValues := [50, 30, 70, 20, 40, 60, 80];
  LRoot := nil;
  for X in LValues do
    LRoot := Insert(LRoot, X);
  Write('árvore em ordem:');
  for X in LGen.InOrder(LRoot) do Write(' ', X);
  WriteLn;

  // uso manual: MoveNext avança, Current lê. Quem cria o iterador manualmente o libera.
  LIter := LGen.Squares(1000000);        // um milhão de quadrados... que nunca serão calculados
  LTotal := 0;
  while LIter.MoveNext do
  begin
    LTotal := LTotal + LIter.Current;
    if LIter.Current > 50 then Break;    // parar cedo é barato: o resto nem existe
  end while;
  WriteLn('soma até passar de 50: ', LTotal);
  LIter.Free;

  LRoot.Free;
  LGen.Free;
end.
Iterators.config
Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extras
Saída
quadrados: 1 4 9 16 25
Collatz de 6: 6 3 10 5 16 8 4 2 1
árvore em ordem: 20 30 40 50 60 70 80
soma até passar de 50: 204

Demo 47. Quadrados, Collatz com estado entre yields, árvore em ordem com yield from, e uso manual com MoveNext.

Corrotinas stackful#

TGenerator<T> recebe um procedimento anônimo e roda o corpo numa corrotina com a sua pilha. O Yield é um método comum, então pode ficar numa rotina auxiliar recursiva (Walk), algo que o iterator function não faz sem yield from.

Coroutines.fxp
program Coroutines;
// Demo: corrotinas "stackful" (Fenner.Coroutines). Cada corrotina tem a SUA pilha, então ela pode
// suspender (Yield) de qualquer profundidade de chamada, inclusive dentro de recursão.
// Diferença para o `iterator function`: aqui o Yield é um método comum e pode estar numa rotina auxiliar.
// Limitação: só funciona em Windows 64 bits (TCoroutine.IsSupported diz se há suporte).

uses System.Remainder, Fenner.Coroutines;   // o Coroutines.config põe Compiler\extras no SourcePath

type
  TNode = class(TObject)
  public
    Value: Int32;
    Left, Right: TNode;
    constructor Create(const AValue: Int32);
    destructor Destroy; override;
  end class;

constructor TNode.Create(const AValue: Int32);
begin
  inherited Create;
  Value := AValue;
end constructor;

destructor TNode.Destroy;
begin
  Left.Free;
  Right.Free;
  inherited Destroy;
end destructor;

function Insert(const ARoot: TNode; const AValue: Int32): TNode;
begin
  if ARoot = nil then
    Exit(TNode.Create(AValue));
  if AValue < ARoot.Value then
    ARoot.Left := Insert(ARoot.Left, AValue)
  else
    ARoot.Right := Insert(ARoot.Right, AValue);
  Result := ARoot;
end function;

// Rotina RECURSIVA comum que faz Yield no meio: um `iterator function` precisaria de `yield from`,
// mas aqui é só uma chamada de método, em qualquer profundidade.
procedure Walk(const ANode: TNode; const AGen: TGenerator<Int32>);
begin
  if ANode = nil then
    Exit;
  Walk(ANode.Left, AGen);
  AGen.Yield(ANode.Value);
  Walk(ANode.Right, AGen);
end procedure;

function Squares(const ACount: Int32): TGenerator<Int32>;
begin
  Result := TGenerator<Int32>.Create(
    procedure(const AGen: TGenerator<Int32>);
    var
      I: Int32;
    begin
      for I := 1 to ACount do
        AGen.Yield(I * I);
    end);
end function;

var
  LGen: TGenerator<Int32>;
  LRoot: TNode;
  LValues: array of Int32;
  X, LTotal: Int32;
begin
  WriteLn('suporte a corrotinas: ', TCoroutine.IsSupported);

  LGen := Squares(6);
  Write('quadrados:');
  for X in LGen do Write(' ', X);
  WriteLn;
  LGen.Free;

  LValues := [50, 30, 70, 20, 40, 60, 80];
  LRoot := nil;
  for X in LValues do
    LRoot := Insert(LRoot, X);
  LGen := TGenerator<Int32>.Create(
    procedure(const AGen: TGenerator<Int32>);
    begin
      Walk(LRoot, AGen);
    end);
  Write('árvore em ordem:');
  for X in LGen do Write(' ', X);
  WriteLn;
  LGen.Free;

  // Parar no meio: o Free CANCELA a corrotina suspensa e libera o que ela segurava.
  LGen := Squares(1000000);
  LTotal := 0;
  for X in LGen do
  begin
    LTotal := LTotal + X;
    if X > 50 then Break;
  end for;
  WriteLn('soma até passar de 50: ', LTotal);
  LGen.Free;
  LRoot.Free;
end.
Coroutines.config
Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extras
Saída
suporte a corrotinas: TRUE
quadrados: 1 4 9 16 25 36
árvore em ordem: 20 30 40 50 60 70 80
soma até passar de 50: 204

Demo 48. O mesmo percurso de árvore com Yield dentro de uma rotina recursiva comum.

Break num for … in e Free cancelam a corrotina suspensa, desenrolando os try/finally e defer que ela tinha abertos.

Armadilhas#

  • Corrotinas só em Windows 64 bits. Em outras plataformas, TCoroutine.Create levanta ECoroutineUnsupported; consulte TCoroutine.IsSupported antes (o demo imprime).
  • Com TIterator criado à mão, você libera (Free); no for … in o compilador cuida.
  • Um gerador que guarda objetos os mantém vivos (ARC) enquanto estiver suspenso.
  • Escolha pela necessidade: iterator function para sequências simples e portáveis; corrotina quando o yield precisa sair de dentro de chamadas aninhadas.

Veja também#