Pular para o conteúdo

Especificação §11

Generators (yields)

language-design.md §11 · 45 linhas · 2 min de leitura

Generators são para sequências lazy e iteração dentro de um único processo. Não são para concorrência, que é papel de processos e channels.

Coroutine-style (bidirecional) é redundante: um “acumulador” que recebe e retorna valores é só um processo com um channel Channel[<-In, ->Out].

@generator fn fibonacci() -> int {
var a, b := 0, 1
loop {
yield a
a, b = b, a + b
}
}
loop n in fibonacci().take(10) { ... }

Restrição (stackless). Sendo zero-alocação e no mesmo processo (acima), o generator é uma state-machine stackless, então yield só acontece no nível do próprio generator, não dentro de uma função comum que ele chame (uma função comum não tem como suspender o generator que a chamou). Para suspensão arbitrária através de chamadas, o que você quer é um processo (stackful, seção 3), não um generator. Com o scheduler ele compõe bem: um next() que toca io.* suspende o processo (stackful), e a state-machine do generator é preservada nessa suspensão.

Abandonar um generator roda os defers dele. O consumidor pode parar antes — break no laço, return de dentro dele, ou esgotar um take(n) antes da fonte secar — e o generator é então desmontado no ponto onde estava suspenso, rodando tudo que devia ali, do escopo mais interno pra fora e cada escopo em LIFO. Um generator que adquire um recurso portanto o libera mesmo que ninguém o drene:

@generator fn linhas(caminho: string) -> string {
f := open(caminho)
defer f.close() // roda mesmo se o consumidor der break na linha 3
...
}

É a mesma regra que a morte de processo segue (seção 8): um escopo que nunca vai ser retomado ainda deve suas limpezas. Sem isso o pipeline lazy seria um vazamento esperando pra acontecer, e o defer deixaria de ser a resposta pra “libera isso, aconteça o que acontecer”. Ser stackless é o que faz isso sair barato: como o defer é escopado por bloco, o que uma suspensão deve é sabido em tempo de compilação, então a desmontagem é código comum num ponto conhecido, não um desenrolador de runtime.

lines_from_file("log.txt")
.filter(fn(l) => l.contains("ERROR"))
.map(fn(l) => parse_entry(l))
.take(100)

Cada estágio é um Generator[T]. Zero alocação intermediária, tudo no mesmo processo. Impossível de fazer com a mesma eficiência via processos e channels.