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)が安定して利用可能なため、プロジェクトの要件に応じて適切な方を選択できます。