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 function | TGenerator<T> (Fenner.Coroutines) | |
|---|---|---|
| Mecanismo | máquina de estados gerada pelo compilador | corrotina com pilha própria (uma fiber do Windows) |
| Onde pode suspender | só no corpo da própria função | de qualquer profundidade de chamada |
| Recursão | precisa de yield from | uma chamada comum a Yield em rotina recursiva |
| Biblioteca | Fenner.Iterators | Fenner.Coroutines |
| Plataforma | não depende de fibers do sistema | só Windows 64 bits |
Ambos moram em Compiler\extras; o .config do projeto o põe no SourcePath.
iterator function e yield#
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 25O 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.
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.Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extrasquadrados: 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: 204Demo 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.
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.Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\extrassuporte 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: 204Demo 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.CreatelevantaECoroutineUnsupported; consulteTCoroutine.IsSupportedantes (o demo imprime). - Com
TIteratorcriado à mão, você libera (Free); nofor … ino compilador cuida. - Um gerador que guarda objetos os mantém vivos (ARC) enquanto estiver suspenso.
- Escolha pela necessidade:
iterator functionpara sequências simples e portáveis; corrotina quando oyieldprecisa sair de dentro de chamadas aninhadas.
Veja também#
- Lambdas, eventos e async e async/await e Fenner.Tasks.
- Demos
47-iteradorese48-corrotinas.