Generators
stream action e emit: sequências produzidas sob demanda, inclusive infinitas.
A forma#
stream action contar(ate):
cycle i from 1 to ate:
emit i
s := contar(5)
out typeof(s) # Stream
out s.to_cluster() # [1, 2, 3, 4, 5]
out contar(1000).take(3) # [1, 2, 3]| Palavra | Efeito |
|---|---|
stream action | a chamada devolve um Stream, não um valor |
emit | produz um item e continua de onde parou |
emit e yield são coisas diferentes#
action f():
yield 1 # devolve 1 e ENCERRA a ação
stream action g():
emit 1 # produz 1 e CONTINUA
emit 2Em Python as duas ideias dividem a mesma palavra (yield), o que é uma fonte conhecida de confusão — uma função vira geradora só por conter um yield, sem nada no cabeçalho anunciando isso.
DataForge separa: stream action no cabeçalho diz o que a ação é, emit diz o que ela faz. Dentro de um stream action, yield ainda serve para encerrar a produção antes do fim.
Sequências infinitas#
Um persist yes: dentro de um stream action não trava o programa:
stream action naturais():
n := 0
persist yes:
emit n
n += 1
out naturais().take(6) # [0, 1, 2, 3, 4, 5]Por que funciona#
O corpo não roda na chamada. naturais() devolve um Stream sem executar nada. A execução acontece sob demanda: cada emit roda quando alguém pede o próximo item, e pausa logo depois.
take(6) pede seis itens, recebe seis, e para de pedir. O laço infinito simplesmente nunca chega à sétima volta.
Fibonacci#
stream action fibonacci():
a := 0
b := 1
persist yes:
emit a
a, b := b, a + b
out fibonacci().take(10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Essa definição é completa e não menciona limite algum. Quem chama decide. Repare no a, b := b, a + b — a desestruturação faz a troca simultânea sem variável temporária.
Consumir#
| Chamada | Devolve |
|---|---|
s.to_cluster() | tudo, como lista |
s.take(n) | os n primeiros |
s.next() | o próximo item, ou void |
s.count() | quantos itens ao todo |
s.first() | o primeiro, ou void |
s.map(f) s.filter(f) | lista transformada ou filtrada |
s.reset() | reinicia o next() |
cycle v in s: | percorre item a item |
Parar por dentro#
stream action ate_passar(limite):
n := 1
persist yes:
given n bigger limite:
halt
emit n
n *= 3
out ate_passar(100).to_cluster() # [1, 3, 9, 27, 81]Agora to_cluster() é seguro: a sequência termina sozinha.
Encadear#
Aqui está o padrão que dá poder à ideia — um generator que consome outro:
stream action so_pares(fonte):
cycle v in fonte:
given v % 2 is 0:
emit v
stream action dobrar(fonte):
cycle v in fonte:
emit v * 2
cadeia := dobrar(so_pares(naturais()))
out cadeia.take(5) # [4, 8, 12, 16, 20]naturais() é infinito. so_pares filtra, dobrar transforma — e nada roda até o take(5). Cada item atravessa a cadeia inteira sob demanda; a memória usada não depende do tamanho da fonte.
É o mesmo desenho dos pipes do Unix: yes | grep … | sed … não trava porque cada estágio consome o anterior aos poucos.
Processamento incremental#
O ganho fica óbvio quando a fonte é grande:
stream action linhas_do_log():
cycle l in IO.read("app.log").lines():
emit l
stream action interpretar(fonte):
cycle linha in fonte:
partes := linha.split(" ")
emit {"data": partes[0], "nivel": partes[1]}
stream action apenas(fonte, nivel):
cycle registro in fonte:
given registro["nivel"] is nivel:
emit registro
primeiro_erro := apenas(interpretar(linhas_do_log()), "ERROR").first()Isso lê até o primeiro erro e para. Se ele estiver na linha 3, as outras 999.997 nunca são tocadas.
emit fora de um generator#
Fora do corpo de um stream action, emit é um alias histórico de out — ele imprime. Isso mantém compatível o código escrito antes do 4.0.