Layers e dependências
Até aqui, o R apareceu no tipo (Effect<A, E, R>) e no contágio, mas sempre como símbolo: a terceira letra que sobe pela composição e some quando você providencia algo. Falta ver o que ele realmente é. Não é um parâmetro genérico qualquer; é o canal onde mora a injeção de dependências do Effect.
No Effect, injeção de dependências não é uma biblioteca à parte, é parte do tipo. O terceiro parâmetro de Effect<A, E, R> (o R) é o conjunto de dependências. Este é o mecanismo de DI idiomático do Effect: nada de container singleton, nada de framework de DI. O ciclo completo é declarar → usar → implementar → fornecer, e ele substitui o container dos frameworks tradicionais porque quem garante o grafo é o compilador. Vale ter lido antes a descrição e a execução, já que o R sobe pela composição junto com o resto.
As três peças
Section titled “As três peças”| Peça | Papel | Analogia |
|---|---|---|
Context.Tag | declara que serviço existe (interface + chave) | a interface + o token de DI |
Layer | descreve como construir esse serviço | o provider / módulo de DI |
Context | o “mapa” de serviços construídos, injetado no runtime | o container |
Você quase nunca mexe no Context (o mapa) diretamente. Trabalha com Tag
(declarar/usar) e Layer (construir/compor).
Num layout típico em camadas, os contratos (Context.Tag) vivem em domain/, as
implementações (Layer) vivem em infra/, e a composição final (AppLive)
vive num único arquivo, como src/layers.ts.
Declarar um serviço (via CLASSE)
Section titled “Declarar um serviço (via CLASSE)”A forma idiomática é estender Context.Tag. A classe serve como interface,
como identificador único de DI e como acessor, tudo de graça.
import { Effect, Context } from "effect"
class Clock extends Context.Tag("MyApp/Clock")< Clock, // 1. o Self (a própria classe) { readonly now: Effect.Effect<number> } // 2. o Shape (a interface do serviço)>() {}- O primeiro parâmetro de tipo é o Self.
- O segundo é o Shape: o formato do serviço.
- A string
"MyApp/Clock"é o identificador único (use prefixo namespaced).
❌
Context.GenericTag<T>("Nome")é a versão de baixo nível. Funciona, mas a classe acima é a recomendada: dá o tipo opaco e o identificador único sem boilerplate.
Usando o serviço
Section titled “Usando o serviço”Fazer yield* na tag te dá a implementação, e adiciona Clock ao canal R
do Effect automaticamente:
const program = Effect.gen(function* () { const clock = yield* Clock return yield* clock.now})// tipo inferido: Effect<number, never, Clock>// ^^^^^ o requirement apareceu sozinhoO compilador agora exige que você forneça um Clock antes de rodar. Esquecer
uma dependência é erro de compilação, não de runtime.
Implementar: construindo Layers
Section titled “Implementar: construindo Layers”Um Layer<ROut, E, RIn> é uma receita que produz os serviços ROut, podendo
falhar com E e podendo precisar de outros serviços RIn para se construir.
Layer.succeed: implementação pronta (sem efeito)
Section titled “Layer.succeed: implementação pronta (sem efeito)”import { Layer } from "effect"
const ClockLive = Layer.succeed(Clock, { now: Effect.sync(() => Date.now()),})// Layer<Clock>Layer.effect: construção que exige efeito (ler config, etc.)
Section titled “Layer.effect: construção que exige efeito (ler config, etc.)”const DatabaseLive = Layer.effect( Database, Effect.gen(function* () { const url = yield* Config.redacted("DATABASE_URL") const pool = createPool(Redacted.value(url)) return { query: (sql) => Effect.promise(() => pool.query(sql)) } }))// Layer<Database, ConfigError, never>Layer.scoped: serviço com ciclo de vida (fecha no shutdown)
Section titled “Layer.scoped: serviço com ciclo de vida (fecha no shutdown)”Use quando a construção adquire um recurso que precisa ser liberado. É assim que o pool do banco costuma ser gerido.
const DatabaseLive = Layer.scoped( Database, Effect.gen(function* () { const url = yield* Config.redacted("DATABASE_URL") const pool = yield* Effect.acquireRelease( Effect.sync(() => createPool(Redacted.value(url))), (pool) => Effect.promise(() => pool.end()) // liberado no shutdown ) return { query: (sql) => Effect.promise(() => pool.query(sql)) } }))O atalho: Effect.Service
Section titled “O atalho: Effect.Service”Effect.Service junta declaração + Layer numa classe só. Ótimo para reduzir
boilerplate quando o serviço tem uma implementação principal.
import { Effect } from "effect"
class Database extends Effect.Service<Database>()("MyApp/Database", { effect: Effect.gen(function* () { const url = yield* Config.redacted("DATABASE_URL") const pool = createPool(Redacted.value(url)) return { query: (sql: string) => Effect.promise(() => pool.query(sql)) } }), dependencies: [], // outros Layers que este serviço precisa}) {}
Database // a Tag (usa com yield* Database)Database.Default // o Layer pronto (Layer<Database>)Variações do segundo argumento: succeed, sync, effect, scoped (use scoped
quando precisar liberar recurso).
Quando usar qual?
Effect.Service→ o caso comum (um serviço, uma implementação principal).Context.Tag(emdomain/) +Layerseparado (eminfra/) → quando você quer várias implementações da mesma interface (ex.:DatabaseLivevsDatabaseTest) sem acoplá-las. É o padrão que melhor casa com um layout em camadas.
Compor Layers: o grafo de dependências
Section titled “Compor Layers: o grafo de dependências”Layer.provide: satisfazer as dependências de um layer
Section titled “Layer.provide: satisfazer as dependências de um layer”Se UserRepository precisa de Database, você “alimenta” um com o outro:
const UserRepositoryLive = Layer.effect( UserRepository, Effect.gen(function* () { const db = yield* Database // consome Database return { findById: (id) => /* ...usa db... */ } }))
const RepoWithDb = UserRepositoryLive.pipe( Layer.provide(DatabaseLive) // injeta Database; ele some do tipo (foi satisfeito))// Layer<UserRepository, ConfigError, never>Layer.merge: juntar layers lado a lado (irmãos)
Section titled “Layer.merge: juntar layers lado a lado (irmãos)”const Infra = Layer.merge(DatabaseLive, LoggerLive) // Layer<Database | Logger>Layer.provideMerge: prover E também expor
Section titled “Layer.provideMerge: prover E também expor”Como provide, mas mantém o provedor visível na saída.
const AppInfra = UserRepositoryLive.pipe( Layer.provideMerge(DatabaseLive) // expõe UserRepository E Database)Regra de ouro:
provide= “escondo a dependência (encapsulo)”.merge/provideMerge= “exponho também”. Comece comprovide; só exponha o que camadas superiores realmente consomem.
Memoização automática
Section titled “Memoização automática”Se dois layers dependem do mesmo DatabaseLive, o Effect constrói o Database
uma vez só e compartilha, desde que seja a mesma referência de layer. Isso
evita duas conexões de banco por acidente. (Por isso defina cada *Live uma vez e
reutilize a referência.)
O grafo completo da aplicação
Section titled “O grafo completo da aplicação”Tudo é montado num único arquivo, src/layers.ts:
import { Layer } from "effect"
const InfraLive = Layer.mergeAll( DatabaseLive, // Layer<Database, ConfigError> LoggerLive, ClockLive,)
const ReposLive = Layer.mergeAll( UserRepositoryLive, PostRepositoryLive,).pipe(Layer.provide(InfraLive))
// o layer final da aplicação (consumido pela camada HTTP)export const AppLive = ReposLiveA leitura é de baixo para cima: cada camada provide a de baixo, escondendo os
detalhes. Isto substitui por completo o “container singleton” que templates
sem-Effect usam, com a diferença de que aqui o compilador garante o grafo.
Fornecer ao programa e rodar
Section titled “Fornecer ao programa e rodar”Effect.provide injeta um layer, removendo aqueles requirements do canal R.
Quando R chega a never, o programa pode rodar.
import { Effect } from "effect"
const program = Effect.gen(function* () { const users = yield* UserRepository return yield* users.findAll()})// Effect<User[], DbError, UserRepository>
const runnable = program.pipe(Effect.provide(AppLive))// Effect<User[], DbError, never> ← R virou never, agora rodaNo servidor HTTP, o AppLive é fornecido ao layer do servidor uma vez, no
ponto de entrada da aplicação.
Troca de implementação (test doubles)
Section titled “Troca de implementação (test doubles)”Como o serviço é só uma tag, trocar por um mock em teste é trivial, sem framework de mock:
const DatabaseTest = Layer.succeed(Database, { query: () => Effect.succeed([{ id: 1, name: "fake" }]),})
const TestLive = ReposLive.pipe( Layer.provide(Layer.merge(DatabaseTest, ClockTest)))Injeção por tipo: o compilador garante que você forneceu tudo, e você troca qualquer peça por outra do mesmo Shape. É o que torna um serviço trivialmente substituível numa suíte de testes.
Nota v4
Section titled “Nota v4”No v4 (beta), toda a família de declaração de serviço (Context.Tag,
Context.GenericTag, Effect.Tag, Effect.Service) é unificada em
Context.Service. A mecânica de Layer permanece conceitualmente igual.
Escrever no dialeto reduzido (classe + Layer/Effect.Service) hoje é o caminho
de menor atrito para essa migração. Continue no v3 em produção.
Palavras-chave
Section titled “Palavras-chave”Conceitos: injeção de dependências, canal R, requirement, grafo de dependências, memoização de layer, test double, container substituído pelo compilador
Declarar: Context.Tag, Context.GenericTag, Self, Shape, prefixo namespaced
Implementar: Layer.succeed, Layer.effect, Layer.scoped, Effect.Service, Default, acquireRelease
Compor: Layer.provide, Layer.merge, Layer.provideMerge, Layer.mergeAll, AppLive
Fornecer: Effect.provide, R para never
Versões: v3.x, v4 beta, Context.Service