Skip to content

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.

PeçaPapelAnalogia
Context.Tagdeclara que serviço existe (interface + chave)a interface + o token de DI
Layerdescreve como construir esse serviçoo provider / módulo de DI
Contexto “mapa” de serviços construídos, injetado no runtimeo 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.

A forma idiomática é estender Context.Tag. A classe serve como interface, como identificador único de DI e como acessor, tudo de graça.

src/domain/clock/index.ts
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.

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 sozinho

O compilador agora exige que você forneça um Clock antes de rodar. Esquecer uma dependência é erro de compilação, não de runtime.

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)) }
})
)

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 (em domain/) + Layer separado (em infra/) → quando você quer várias implementações da mesma interface (ex.: DatabaseLive vs DatabaseTest) sem acoplá-las. É o padrão que melhor casa com um layout em camadas.

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 com provide; só exponha o que camadas superiores realmente consomem.

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

Tudo é montado num único arquivo, src/layers.ts:

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 = ReposLive

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

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 roda

No servidor HTTP, o AppLive é fornecido ao layer do servidor uma vez, no ponto de entrada da aplicação.

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.

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.

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