tags :
Effect.ts における依存性注入と Context システム
Effect 型の基礎
Effect は計算の記述を表す中心的な型で、3つの型パラメータを持ちます:
Effect<Success, Error, Requirements>- Success: 成功時の値の型
- Error: 失敗時のエラーの型
- Requirements: 実行に必要な依存性(Context)の型
import { Effect } from "effect"
// Effect<number, never, never> - 純粋な計算
const pure = Effect.succeed(42)
// Effect<number, Error, never> - 失敗する可能性がある計算
const mayFail = Effect.try(() => {
if (Math.random() > 0.5) throw new Error("Failed")
return 42
})Context システム(依存性注入)
Context は Effect の型安全な依存性注入システムの中核です。
サービスの定義方法
-
Context.GenericTag(関数ベース)
文字列識別子を使用してサービスタグを作成します:
import { Context, Effect } from "effect" interface Logger { readonly log: (message: string) => Effect.Effect<void> } // 文字列識別子でタグを作成 const Logger = Context.GenericTag<Logger>("Logger") // 具体的な実装 const ConsoleLogger: Logger = { log: (message) => Effect.sync(() => console.log(message)) }
-
Context.Tag(クラスベース)
クラス継承を使用してサービスタグを作成します:
import { Context, Effect } from "effect" // 方法1: インターフェースと実装が同じ場合 class Logger extends Context.Tag("Logger")<Logger, { readonly log: (message: string) => Effect.Effect<void> }>() {} // 方法2: インターフェースを分離する場合 interface IDatabase { readonly query: (sql: string) => Effect.Effect<any[]> } class Database extends Context.Tag("Database")<Database, IDatabase>() {}
依存性の取得
// Generator 構文での取得
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.log("Hello, Effect!")
})
// pipe を使った取得
const program2 = Logger.pipe(
Effect.flatMap((logger) => logger.log("Hello again!"))
)Service の実装
より実践的なサービスの例:
interface UserRepository {
readonly findById: (id: string) => Effect.Effect<User | null, DatabaseError>
readonly save: (user: User) => Effect.Effect<void, DatabaseError>
}
const UserRepository = Context.GenericTag<UserRepository>("UserRepository")
// PostgreSQL 実装
const PostgresUserRepository: UserRepository = {
findById: (id) => Effect.tryPromise({
try: () => db.query(`SELECT * FROM users WHERE id = $1`, [id]),
catch: (error) => new DatabaseError({ cause: error })
}),
save: (user) => Effect.tryPromise({
try: () => db.query(`INSERT INTO users ...`, [user]),
catch: (error) => new DatabaseError({ cause: error })
})
}Layer(依存性の提供と合成)
Layer はサービスの実装を提供し、依存関係を管理するための仕組みです。
基本的な Layer
import { Layer } from "effect"
// シンプルな Layer
const LoggerLive = Layer.succeed(Logger, ConsoleLogger)
// 依存性を持つ Layer
const UserServiceLive = Layer.effect(
UserService,
Effect.gen(function* () {
const logger = yield* Logger
const repo = yield* UserRepository
return {
getUser: (id: string) =>
Effect.gen(function* () {
yield* logger.log(`Getting user ${id}`)
return yield* repo.findById(id)
})
}
})
)Layer の合成
// 複数の Layer を合成
const AppLayer = Layer.mergeAll(
LoggerLive,
DatabaseLive,
UserRepositoryLive
)
// 依存関係の解決
const MainLayer = UserServiceLive.pipe(
Layer.provide(AppLayer)
)依存性のマッピングと変換
サービスの変換
// 既存のサービスを拡張
const EnhancedLogger = Layer.map(
LoggerLive,
(logger) => ({
...logger,
logWithTimestamp: (message: string) =>
logger.log(`[${new Date().toISOString()}] ${message}`)
})
)部分的な依存性の提供
// Effect.provideService - 特定のサービスだけを提供
const partiallyProvided = program.pipe(
Effect.provideService(Logger, ConsoleLogger)
// 他のサービスはまだ必要
)
// Layer.provideSome - 部分的な Layer 合成
const PartialLayer = UserServiceLive.pipe(
Layer.provideSome(LoggerLive)
// UserRepository はまだ必要
)完全なアプリケーション例
// アプリケーション全体の構成
const program = Effect.gen(function* () {
const logger = yield* Logger
const userService = yield* UserService
yield* logger.log("Application started")
const user = yield* userService.getUser("123")
if (user) {
yield* logger.log(`Found user: ${user.name}`)
}
})
// すべての依存性を提供して実行
const runnable = program.pipe(
Effect.provide(MainLayer),
Effect.catchAll((error) =>
Effect.sync(() => console.error("Error:", error))
)
)
// 実行
Effect.runPromise(runnable)高度なパターン
環境別の実装切り替え
const LoggerTest = Layer.succeed(Logger, TestLogger)
const LoggerProd = Layer.succeed(Logger, ConsoleLogger)
const AppLayer = isProduction ? LoggerProd : LoggerTestリソースのライフサイクル管理
const DatabaseLayer = Layer.scoped(
Database,
Effect.acquireRelease(
// リソースの取得
Effect.sync(() => new DatabaseConnection()),
// リソースの解放
(conn) => Effect.sync(() => conn.close())
)
)Context.Tag vs Context.GenericTag の選択
Context.Tag(クラスベース)を選ぶ場合
- 新規プロジェクト
- より強い型安全性が必要
- 静的メソッドを追加したい
- IDE のサポートを最大限活用したい
- ホットリロード環境での安定性が重要
Context.GenericTag(関数ベース)を選ぶ場合
- 既存コードとの互換性が必要
- シンプルで直感的な API を好む
- 関数型プログラミングスタイルを重視
- 軽量な実装で十分
まとめ
Effect.ts の依存性注入システムは、型安全性を保ちながら、テスタブルで保守しやすいコードを書くための強力な仕組みを提供します。Context と Layer を組み合わせることで、複雑な依存関係も明確に管理できます。
最新の Effect 3.x では両方の API(Context.Tag と Context.GenericTag)が安定して利用可能なため、プロジェクトの要件に応じて適切な方を選択できます。