mcpField
E
effect-ts

effect-ts

github.com/sablier-labs开发952 次安装6 周前更新
代码仓库

关于 effect-ts

effect-ts 是一项为使用 Effect 库开发提供指导的 skill,面向 Effect 项目开发者。内容涵盖类型化错误处理、依赖注入、组合、资源管理、并发及测试模式。

主要功能

  1. 01类型化错误处理模式
  2. 02用于依赖注入的 Services 和 Layers
  3. 03可组合的 Effect 与资源作用域
  4. 04并发、流和调度指导
  5. 05测试及代码模式研究

适用场景

  1. 01设计 Effect 程序的错误处理
  2. 02为应用添加服务与层
  3. 03管理异步任务和资源
  4. 04应用 Effect 测试模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/sablier-labs/agent-skills --skill effect-ts

安装后在 Claude Code 中输入/effect-ts确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
critical-rules.md
effect-atom.md
next-js.md
option-null.md
streams.md
testing.md

详细介绍

Effect-TS 专家

提供使用 Effect 库进行函数式编程的专家指导,涵盖错误处理、依赖注入、可组合性和测试模式。

前置条件检查

开始任何与 Effect 相关的工作之前,请确认 Effect-TS 源代码存在于 ~/.effect。

如果不存在,请立即停止并告知用户。 继续之前先克隆它:

git clone https://github.com/Effect-TS/effect.git ~/.effect

研究策略

Effect-TS 有多种方式可以完成同一项任务。使用 Effect 模式时,请主动通过 Task 工具生成研究 Agent,调查最佳实践,尤其是在处理中等到高复杂度任务时。

研究来源(按优先级排序)
  1. 优先查看代码库模式 — 实现前,先检查当前项目中类似的模式。如果代码库中存在 Effect 模式,请遵循这些模式以保持一致。如果不存在,则跳过此步骤。

  2. Effect 源代码 — 遇到复杂的类型错误、行为不明确或实现细节问题时,请查看 ~/.effect/packages/effect/src/ 中的 Effect 源代码。这里包含 Effect 核心逻辑和模块。

何时进行研究

高优先级(始终研究):

  • 实现 Service、Layer 或复杂的依赖注入
  • 处理包含多种错误类型或复杂错误层次结构的错误
  • 基于 Stream 的操作和响应式模式
  • 使用作用域 Effect 进行资源管理和清理
  • 并发/并行操作和性能关键型代码
  • 测试模式,尤其是不熟悉的测试场景

中优先级(复杂时研究):

  • 将命令式代码(try-catch、Promise)重构为 Effect 模式
  • 添加新的 Service 依赖或重构 Service 层
  • 自定义错误类型或扩展现有错误层次结构
  • 与外部系统(数据库、API、第三方服务)集成
研究方法
  • 调查多个相关模式时,同时生成多个 Task Agent
  • 优先寻找规范、易读且易维护的解决方案,而非巧妙的优化
  • 若存在相关模式,请依据现有代码库模式验证建议方案的一致性
  • 存在多种可行方案时,请研究并找出最符合 Effect-TS 惯用方式的解决方案

代码库模式发现

在使用 Effect 的项目中工作时,实现新代码前请检查现有模式:

  1. 搜索 Effect 导入 — 查找从 'effect' 导入的文件,以了解现有用法
  2. 识别 Service 模式 — 找出项目中 Service 和 Layer 的组织方式
  3. 留意错误处理约定 — 检查错误的定义和传播方式
  4. 检查测试模式 — 查看项目中 Effect 代码的测试方式

如果代码库中不存在 Effect 模式,请采用 Effect 源代码和示例中的规范模式继续工作。不要因缺少代码库模式而停滞。

Effect 原则

编写 Effect 代码时,请遵循以下核心原则:

错误处理
  • 使用 Effect 的类型化错误系统,而非抛出异常
  • 定义描述明确的错误类型,并妥善传播错误
  • 使用 Effect.fail、Effect.catchTag、Effect.catchAll 控制错误流程
  • 禁止使用的模式请参阅 ./references/critical-rules.md
依赖注入
  • 使用 Service 和 Layer 实现依赖注入
  • 使用 Context.Tag 定义 Service
  • 使用 Layer.merge、Layer.provide 组合 Layer
  • 使用 Effect.provide 注入依赖
可组合性
  • 利用 Effect 的可组合性处理复杂操作
  • 使用适当的构造器:Effect.succeed、Effect.fail、Effect.tryPromise、Effect.try
  • 使用作用域 Effect 进行适当的资源管理
  • 使用 Effect.flatMap、Effect.map、Effect.tap 串联操作
代码质量
  • 编写类型安全的代码,充分利用 Effect 的类型系统
  • 使用 Effect.gen 编写清晰易读的顺序代码
  • 使用 Effect 的测试工具实现恰当的测试模式
  • 优先使用 Effect.fn(),以自动收集遥测数据并获得更好的堆栈跟踪

关键规则

编写任何 Effect 代码之前,请阅读并牢记 ./references/critical-rules.md。关键指南:

  • 无效做法: 在 Effect.gen 中使用 try-catch(Effect 失败不会被抛出)
  • 避免: 类型断言(as never/any/unknown)
  • 推荐: 使用 return yield* 模式处理错误(明确表示执行终止)

常见失败模式

快速查看经常导致问题的模式:

  • SubscriptionRef 版本不匹配 — unsafeMake is not a function → 快速参考
  • 取消与失败 — 中断不是错误 → 错误分类
  • Option 与 null — 内部使用 Option,边界处使用 null → option-null.md
  • Stream 背压 — 无限流会挂起 → streams.md

解释解决方案

提供解决方案时,请解释所用的 Effect-TS 概念,以及这些概念为何适用于特定用例。如果遇到文档未涵盖的模式,请在保持与现有代码库模式一致(如有)的前提下,提出改进建议。

快速参考

创建 Effect
Effect.succeed(value)           // Wrap success value
Effect.fail(error)              // Create failed effect
Effect.tryPromise(fn)           // Wrap promise-returning function
Effect.try(fn)                  // Wrap synchronous throwing function
Effect.sync(fn)                 // Wrap synchronous non-throwing function
组合 Effect
Effect.flatMap(effect, fn)      // Chain effects
Effect.map(effect, fn)          // Transform success value
Effect.tap(effect, fn)          // Side effect without changing value
Effect.all([...effects])        // Run effects (concurrency configurable)
Effect.forEach(items, fn)       // Map over items with effects

// Collect ALL errors (not just first)
Effect.all([e1, e2, e3], { mode: "validate" })  // Returns all failures

// Partial success handling
Effect.partition([e1, e2, e3])  // Returns [failures, successes]
错误处理
// Define typed errors with Data.TaggedError (preferred)
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
  userId: string
}> {}

// Direct yield of errors (no Effect.fail wrapper needed)
Effect.gen(function* () {
  if (!user) {
    return yield* new UserNotFoundError({ userId })
  }
})

Effect.catchTag(effect, tag, fn) // Handle specific error tag
Effect.catchAll(effect, fn)      // Handle all errors
Effect.result(effect)            // Convert to Exit value
Effect.orElse(effect, alt)       // Fallback effect
错误分类

按类别划分错误,以便妥善处理:

类别示例处理方式
预期拒绝用户取消、拒绝平稳退出,不重试
领域错误验证、业务规则向用户显示,不重试
缺陷Bug、断言记录日志并告警,进行调查
中断Fiber 取消、超时清理,必要时可重试
未知/外部错误抛出的异常在边界处规范化
// Pattern: Normalize unknown errors at boundary
const safeBoundary = Effect.catchAllDefect(effect, (defect) =>
  Effect.fail(new UnknownError({ cause: defect }))
)

// Pattern: Catch user-initiated cancellations separately
Effect.catchTag(effect, "UserCancelledError", () => Effect.succeed(null))

// Pattern: Handle interruptions differently from failures
Effect.onInterrupt(effect, () => Effect.log("Operation cancelled"))
模式匹配(Match 模块)

用于带标签联合类型和复杂条件的默认分支工具。

import { Match } from "effect"

// Type-safe exhaustive matching on tagged errors
const handleError = Match.type<AppError>().pipe(
  Match.tag("UserCancelledError", () => null),          // Expected rejection
  Match.tag("ValidationError", (e) => e.message),       // Domain error
  Match.tag("NetworkError", () => "Connection failed"), // Retryable
  Match.exhaustive  // Compile error if case missing
)

// Replace nested catchTag chains
// BEFORE: effect.pipe(catchTag("A", ...), catchTag("B", ...), catchTag("C", ...))
// AFTER:
Effect.catchAll(effect, (error) =>
  Match.value(error).pipe(
    Match.tag("A", handleA),
    Match.tag("B", handleB),
    Match.tag("C", handleC),
    Match.exhaustive
  )
)

// Match on values (cleaner than if/else)
const describe = Match.value(status).pipe(
  Match.when("pending", () => "Loading..."),
  Match.when("success", () => "Done!"),
  Match.orElse(() => "Unknown")
)
服务与 Layer
// Pattern 1: Context.Tag (implementation provided separately via Layer)
class MyService extends Context.Tag("MyService")<MyService, { ... }>() {}
const MyServiceLive = Layer.succeed(MyService, { ... })
Effect.provide(effect, MyServiceLive)

// Pattern 2: Effect.Service (default implementation bundled)
class UserRepo extends Effect.Service<UserRepo>()("UserRepo", {
  effect: Effect.gen(function* () {
    const db = yield* Database
    return { findAll: db.query("SELECT * FROM users") }
  }),
  dependencies: [Database.Default],  // Optional service dependencies
  accessors: true                     // Auto-generate method accessors
}) {}
Effect.provide(effect, UserRepo.Default)  // .Default layer auto-generated
// Use UserRepo.DefaultWithoutDependencies when deps provided separately

// Effect.Service with parameters (3.16.0+)
class ConfiguredApi extends Effect.Service<ConfiguredApi>()("ConfiguredApi", {
  effect: (config: { baseUrl: string }) =>
    Effect.succeed({ fetch: (path: string) => `${config.baseUrl}/${path}` })
}) {}

// Pattern 3: Context.Reference (defaultable tags - 3.11.0+)
class SpecialNumber extends Context.Reference<SpecialNumber>()(
  "SpecialNumber",
  { defaultValue: () => 2048 }
) {}
// No Layer required if default value suffices

// Pattern 4: Context.ReadonlyTag (covariant - 3.18.0+)
// Use for functions that consume services without modifying the type
function effectHandler<I, A, E, R>(service: Context.ReadonlyTag<I, Effect.Effect<A, E, R>>) {
  // Handler can use service in a covariant position
}
生成器模式
Effect.gen(function* () {
  const a = yield* effectA;
  const b = yield* effectB;
  if (error) {
    return yield* Effect.fail(new MyError());
  }
  return result;
});

// Effect.fn - automatic tracing and telemetry (preferred for named functions)
const fetchUser = Effect.fn("fetchUser")(function* (id: string) {
  const db = yield* Database
  return yield* db.query(id)
})
// Creates spans, captures call sites, provides better stack traces
资源管理
Effect.acquireUseRelease(acquire, use, release)  // Bracket pattern
Effect.scoped(effect)                            // Scope lifetime to effect
Effect.addFinalizer(cleanup)                     // Register cleanup action
Duration

Effect 在任何预期输入为 DurationInput 的位置都接受易读的时长字符串:

// String syntax (preferred) - singular or plural forms work
Duration.toMillis("5 minutes")    // 300000
Duration.toMillis("1 minute")     // 60000
Duration.toMillis("30 seconds")   // 30000
Duration.toMillis("100 millis")   // 100

// Verbose syntax (avoid)
Duration.toMillis(Duration.minutes(5))  // Same result, more verbose

// Common units: millis, seconds, minutes, hours, days, weeks
// Also: nanos, micros
调度
Effect.retry(effect, Schedule.exponential("100 millis"))  // Retry with backoff
Effect.repeat(effect, Schedule.fixed("1 second"))         // Repeat on schedule
Schedule.compose(s1, s2)                                  // Combine schedules
状态管理
Ref.make(initialValue)       // Mutable reference
Ref.get(ref)                 // Read value
Ref.set(ref, value)          // Write value
Deferred.make<E, A>()        // One-time async value
SubscriptionRef(响应式引用)
// WARNING: Never use unsafeMake - it may not exist in your Effect version.
// If you see "unsafeMake is not a function", use the safe API below.

SubscriptionRef.make(initial)      // Create reactive reference (safe)
SubscriptionRef.get(ref)           // Read current value
SubscriptionRef.set(ref, value)    // Update value (notifies subscribers)
SubscriptionRef.changes(ref)       // Stream of value changes

// React integration (effect-atom pattern)
const ref = yield* SubscriptionRef.make<User | null>(null)
// Hook reads: useSubscriptionRef(ref) — returns current value or null
// Handle null explicitly in components
并发
Effect.fork(effect)              // Run in background fiber
Fiber.join(fiber)                // Wait for fiber result
Effect.race(effect1, effect2)    // First to complete wins
Effect.all([...effects], { concurrency: "unbounded" })
配置与环境变量
import { Config, ConfigProvider, Effect, Layer, Redacted } from "effect"

// Basic config values
const port = Config.number("PORT")                    // Required number
const host = Config.string("HOST").pipe(              // Optional with default
  Config.withDefault("localhost")
)

// Sensitive values (masked in logs)
const apiKey = Config.redacted("API_KEY")             // Returns Redacted<string>
const secret = Redacted.value(yield* apiKey)          // Unwrap when needed

// Nested configuration with prefix
const dbConfig = Config.all({
  host: Config.string("HOST"),
  port: Config.number("PORT"),
  name: Config.string("NAME"),
}).pipe(Config.nested("DATABASE"))                    // DATABASE_HOST, DATABASE_PORT, etc.

// Using config in effects
const program = Effect.gen(function* () {
  const p = yield* Config.number("PORT")
  const key = yield* Config.redacted("API_KEY")
  return { port: p, apiKey: Redacted.value(key) }
})

// Custom config provider (e.g., from object instead of env)
const customProvider = ConfigProvider.fromMap(
  new Map([["PORT", "3000"], ["API_KEY", "secret"]])
)
const withCustomConfig = Effect.provide(
  program,
  Layer.setConfigProvider(customProvider)
)

// Config validation and transformation
const validPort = Config.number("PORT").pipe(
  Config.validate({
    message: "Port must be between 1 and 65535",
    validation: (n) => n >= 1 && n <= 65535,
  })
)
数组操作
import { Array as Arr, Order } from "effect"

// Sorting with built-in orderings (accepts any Iterable)
Arr.sort([3, 1, 2], Order.number)              // [1, 2, 3]
Arr.sort(["b", "a", "c"], Order.string)        // ["a", "b", "c"]
Arr.sort(new Set([3n, 1n, 2n]), Order.bigint)  // [1n, 2n, 3n]

// Sort by derived value
Arr.sortWith(users, (u) => u.age, Order.number)

// Sort by multiple criteria
Arr.sortBy(
  users,
  Order.mapInput(Order.number, (u: User) => u.age),
  Order.mapInput(Order.string, (u: User) => u.name)
)

// Built-in orderings: Order.string, Order.number, Order.bigint, Order.boolean, Order.Date
// Reverse ordering: Order.reverse(Order.number)
工具函数
import { constVoid as noop } from "effect/Function"

// constVoid returns undefined, useful as a no-operation callback
noop()  // undefined

// Common use cases:
Effect.tap(effect, noop)              // Ignore value, just run effect
Promise.catch(noop)                   // Swallow errors
eventEmitter.on("event", noop)        // Register empty handler
弃用项
  • BigDecimal.fromNumber — 改用 BigDecimal.unsafeFromNumber(3.11.0+)
  • Schema.annotations() — 现在会移除先前设置的标识符注解;标识符仅与 schema 的 ast 引用绑定(3.17.10)

补充资源

本地 Effect 资源
  • ~/.effect/packages/effect/src/ — Effect 核心模块及其实现
外部资源
参考文件
  • ./references/critical-rules.md — 禁止的模式和强制约定
  • ./references/effect-atom.md — 用于 React 的 Effect-Atom 响应式状态管理
  • ./references/next-js.md — Effect 与 Next.js 15+ App Router 集成模式
  • ./references/option-null.md — Option 与 null 的边界模式
  • ./references/streams.md — Stream 模式和背压注意事项
  • ./references/testing.md — Vitest 确定性测试模式

评论

支持 Markdown