Pular para o conteúdo

Generators

stream action e emit: sequências produzidas sob demanda, inclusive infinitas.

A forma#

dataforge
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]
PalavraEfeito
stream actiona chamada devolve um Stream, não um valor
emitproduz um item e continua de onde parou

emit e yield são coisas diferentes#

dataforge
action f():
    yield 1        # devolve 1 e ENCERRA a ação

stream action g():
    emit 1         # produz 1 e CONTINUA
    emit 2

Em 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:

dataforge
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#

dataforge
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#

ChamadaDevolve
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#

dataforge
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:

dataforge
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:

dataforge
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.