Skip to content

Generators e pipe

Depois de entender que um programa no Effect é uma árvore de composição, sobra a pergunta prática: como se escreve essa árvore? Há dois estilos, e eles não competem, se dividem o trabalho. Um descreve o fluxo interno do programa, o passo a passo da lógica; o outro descreve as transformações aplicadas ao programa por fora.

Uma intuição comum é achar que os generators são o que torna o Effect possível. Não são. A abstração fundamental é o tipo Effect, e a operação fundamental de composição é o flatMap, que encadeia um efeito ao próximo passando o resultado adiante. Escrito na mão, um programa é uma sequência de flatMap aninhados:

const program = Effect.flatMap(db.findUser(id), (user) =>
Effect.flatMap(email.send(user), () =>
Effect.succeed(user),
),
)

Isso já é um programa completo, sem nenhum generator. O Effect.gen só oferece uma forma mais legível de escrever exatamente a mesma coisa, num estilo que parece imperativo:

const program = Effect.gen(function* () {
const user = yield* db.findUser(id)
yield* email.send(user)
return user
})

Por baixo, cada yield* vira uma chamada de flatMap (e de map, no passo final). O gen não cria nenhum mecanismo novo; ele desaninha o encadeamento e o deixa reto, na ordem em que a gente pensa.

O truque não é estranho: é o mesmo que o JavaScript já faz com promessas. Uma cadeia de .then() é chata de ler aninhada, então a linguagem oferece async/await para escrevê-la reta. O Effect faz o análogo com generators.

// Promise: async/await esconde uma cadeia de .then()
const user = await getUser()
const posts = await getPosts(user.id)
// Effect: gen + yield* esconde uma cadeia de flatMap
const program = Effect.gen(function* () {
const user = yield* getUser()
const posts = yield* getPosts(user.id)
return posts
})

A correspondência é quase termo a termo: onde a Promise usa await, o Effect usa yield*. Há uma diferença que não é de sintaxe, e é justamente a que define o Effect: a Promise começa a executar assim que é criada, enquanto o Effect continua sendo só uma descrição até você chamar Effect.runPromise. O await desembrulha um efeito que já está em curso; o yield* apenas registra, na descrição, “quando este programa rodar, execute também este subprograma e me devolva o resultado”.

pipe: um operador que o TypeScript não tem

Section titled “pipe: um operador que o TypeScript não tem”

Se o gen descreve o fluxo interno, o pipe descreve o que se faz com o programa como um todo. A maioria das operações do Effect tem a forma “recebe um Effect, devolve outro Effect”, ou seja, são transformações sobre a descrição. O pipe encadeia essas transformações:

program.pipe(
Effect.retry(policy),
Effect.timeout("5 seconds"),
Effect.withSpan("load-user"),
)

Quem vem de Elixir, F# ou OCaml estranha a forma, e com razão. Nessas linguagens existe um operador de pipeline nativo, e o mesmo se leria assim:

program
|> retry(policy)
|> timeout("5 seconds")
|> withSpan("load-user")

O TypeScript não tem esse operador, então o pipe é uma simulação dele em forma de método (ou de função: pipe(program, retry(...), timeout(...)) faz o mesmo). Visualmente parece que você está passando uma lista de funções, mas o significado é sequencial: pegue este programa e aplique estas transformações, uma após a outra. Cada passo produz um novo Effect, sem mexer no anterior, exatamente como as derivações de um Array:

const p1 = Effect.gen(/* ... */)
const p2 = p1.pipe(Effect.retry(policy)) // novo Effect
const p3 = p2.pipe(Effect.timeout("5 seconds")) // outro novo Effect

No fim, p3 continua sendo uma descrição, agora com retry e timeout embutidos, e ainda sem ter executado nada. Se um dia o TypeScript ganhar o operador |>, a leitura fica mais natural; enquanto isso, .pipe(...) é a convenção adotada por Effect, RxJS e a vizinhança funcional.

A separação fica clara quando os dois aparecem juntos:

const program = Effect.gen(function* () {
// lógica de negócio: o que o programa faz
const user = yield* getUser(id)
const posts = yield* getPosts(user.id)
return { user, posts }
}).pipe(
// preocupações transversais: como o programa será executado
Effect.retry(policy),
Effect.timeout("5 seconds"),
Effect.provide(AppLive),
)

O gen diz o que o programa faz, passo a passo. O pipe diz como esse programa será executado: com retry, com timeout, com quais dependências. Manter a lógica dentro do gen e as preocupações transversais fora, no pipe, é uma das divisões mais elegantes do Effect, porque deixa o negócio legível e concentra retry, observabilidade e injeção de dependências num lugar só.

A heurística prática que menos gera discussão em revisão é direta: gen para orquestração, pipe para transformações curtas e para as camadas transversais. Vale registrar uma honestidade da própria documentação: os generators são oficialmente “opcionais”, e escrever tudo com pipe/flatMap não está errado, só é menos ergonômico para lógica sequencial. Estilos alternativos como Do, andThen e zip funcionam, mas produzem código mais difícil de ler e revisar do que o consenso gen mais pipe.

Conceitos: açúcar sintático, fluxo interno vs. fluxo externo, transformação, imutabilidade, eager vs. lazy APIs: Effect.gen, yield*, flatMap, map, pipe, Effect.retry, Effect.timeout, Effect.provide Analogias: async/await, cadeia de .then(), operador |> (Elixir, F#, OCaml), RxJS Heurística: gen para orquestração, pipe para transformações curtas