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.
flatMap é o mecanismo; gen é o açúcar
Section titled “flatMap é o mecanismo; gen é o açúcar”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 paralelo com async/await
Section titled “O paralelo com async/await”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 flatMapconst 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 Effectconst p3 = p2.pipe(Effect.timeout("5 seconds")) // outro novo EffectNo 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.
Os dois estilos, e quem faz o quê
Section titled “Os dois estilos, e quem faz o quê”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.
Palavras-chave
Section titled “Palavras-chave”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