一键导入
context-witness
Decide between Context Tag witness and capability patterns for dependency injection, understanding coupling trade-offs
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Decide between Context Tag witness and capability patterns for dependency injection, understanding coupling trade-offs
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Implement reactive state management with Effect Atom for React applications
Generate comprehensive predicates and orders for domain types using typeclass patterns
Design and compose Effect layers for clean dependency management
Implement Effect services as fine-grained capabilities avoiding monolithic designs
Implement typeclasses with curried signatures and dual APIs for both data-first and data-last usage
| name | context-witness |
| description | Decide between Context Tag witness and capability patterns for dependency injection, understanding coupling trade-offs |
Choose between witness (existence) and capability (behavior) patterns for Context Tags.
Some coupling is necessary and good - but move it from hard to soft coupling.
Field exists in the schema - tightly coupled to domain model:
// ❌ HARD COUPLING - Serial is part of the schema
export const PaymentIntent = Schema.Struct({
id: Schema.String,
serial: Schema.String, // In schema = hard coupled
amount: Schema.BigInt
})
// Every PaymentIntent MUST have a serial
// Serialization/validation requires serial
// Cannot create without providing serial
// Schema change needed to remove/change serial
Field removed from schema, only injected in code:
// ✅ SOFT COUPLING - Serial not in schema
export const PaymentIntent = Schema.Struct({
id: Schema.String,
amount: Schema.BigInt
// No serial field!
})
// Serial is a witness - required but injected via Context
class Serial extends Context.Tag("Serial")<Serial, string>() {}
const createPaymentIntent = (amount: bigint) =>
Effect.gen(function* () {
const serial = yield* Serial // Injected from context
// Use serial in business logic, logging, etc.
// but it's not part of the persisted data
yield* Logger.info(`Creating payment intent ${serial}`)
return PaymentIntent.make({ id: generateId(), amount })
})
// Type: Effect<PaymentIntent, never, Serial>
Key insight: schema (hard coupling) => witness (soft coupling)
By removing the field from the schema and injecting it only where needed, you:
When to use witnesses:
Use when you only need to know something exists in the environment:
// Witness - a serial number exists
export class Serial extends Context.Tag("Serial")<Serial, string>() {}
const createPaymentIntent = Effect.gen(function* () {
const serial = yield* Serial // Pull from environment
return PaymentIntent.make({ serial, ...other })
})
// Type: Effect<PaymentIntent, never, Serial>
Use when you need operations:
// Capability - can generate/validate
export class SerialService extends Context.Tag("SerialService")<
SerialService,
{
readonly next: () => string
readonly validate: (s: string) => boolean
}
>() {}
const createPaymentIntent = Effect.gen(function* () {
const svc = yield* SerialService
const serial = svc.next() // Behavior
return PaymentIntent.make({ serial, ...other })
})
// Type: Effect<PaymentIntent, never, SerialService>
| Need | Pattern |
|---|---|
| Just presence/value | Witness |
| Operations/generation | Capability |
| Precondition marker | Witness |
| Side effects | Capability |
| Multiple implementations | Capability |
| Mocking behavior | Capability |
| Correlation ID | Witness |
| Transaction context | Witness |
| Logger | Capability |
| Database | Capability |
Good fits:
Good fits:
now() operationWitnesses are trivial to provide:
const test = myProgram.pipe(
Effect.provideService(Serial, "test-serial-123")
)
Capabilities need implementation:
const test = myProgram.pipe(
Effect.provideService(SerialService, {
next: () => "test-serial-123",
validate: () => true
})
)
Rule of thumb: Remove non-essential fields from schema, inject via witness instead.
Ask yourself: Does this need to be persisted/serialized?
// ✅ Domain model - only persisted data
export const Order = Schema.Struct({
id: Schema.String,
items: Schema.Array(LineItem),
total: Schema.BigInt
// No correlationId - not persisted!
// No timestamp - derived from system!
})
// Witnesses for runtime context
class CorrelationId extends Context.Tag("CorrelationId")<CorrelationId, string>() {}
class RequestId extends Context.Tag("RequestId")<RequestId, string>() {}
// Use in code, not in data
const createOrder = (items: Array<LineItem>) =>
Effect.gen(function* () {
const correlationId = yield* CorrelationId // For tracing
const requestId = yield* RequestId // For logging
const clock = yield* Clock // For timestamp
yield* Logger.info({
message: "Creating order",
correlationId, // Used for tracing
requestId, // Used for logging
timestamp: Clock.currentTimeMillis(clock)
})
// Data only contains what's persisted
return Order.make({
id: generateId(),
items,
total: calculateTotal(items)
})
})
// Type: Effect<Order, never, CorrelationId | RequestId | Clock>
Benefits:
Choose witness for simplicity, capability for flexibility.