已发布 上游基线 400740a 原文 ↗ 在 GitHub 编辑

管理 Layer

学习如何使用 Layer 管理服务依赖,为应用构建高效、清晰的依赖图。

管理 Service页面中,你学习了如何创建依赖某个 Service 才能执行的 Effect,以及如何为该 Effect 提供这个 Service。

然而,如果 Effect 程序中的某个 Service 自身在构建时依赖其他 Service,该怎么办?我们希望避免把这些实现细节泄漏到 Service 接口中。

为了表示程序的“依赖图”并更有效地管理这些依赖,我们可以使用一个强大的抽象,称为 “Layer”。

Layer 充当创建 Service 的构造器,让我们能够在构造期间而非 Service 层面管理依赖。这种方式有助于保持 Service 接口的简洁与专注。

在深入细节之前,让我们先回顾一些关键概念:

概念描述
service可复用的组件,提供特定功能,在应用的不同部分被使用。
service key表示 service 的唯一标识符,让 Effect 能够定位并使用它。
contextService 的集合,类似一个以 service key 为键、service 为值的 map。
layer用于构建 service 的抽象,在构造期间而非 Service 层面管理依赖。

设计依赖图

假设我们正在构建一个 Web 应用。可以想象,对于需要管理配置、日志和数据库访问的应用,其依赖图大致如下:

  • Config service 提供应用配置。
  • Logger service 依赖 Config service。
  • Database service 同时依赖 ConfigLogger service。

我们的目标是构建 Database service 及其直接与间接依赖。这意味着需要确保 Config service 对 LoggerDatabase 都可用,然后把这些依赖提供给 Database service。

避免需求泄漏

在构造 Database service 时,重要的是避免在 Database 接口中暴露对 ConfigLogger 的依赖。

你可能会想按如下方式定义 Database service:

示例(在 Service 接口中泄漏依赖)

import { Effect, Context } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<Config, {}>()("Config") {}

// Declaring a service key for the Logger service
class Logger extends Context.Service<Logger, {}>()("Logger") {}

// Declaring a service key for the Database service
class Database extends Context.Service<
  Database,
  {
    // ❌ Avoid exposing Config and Logger as a requirement
    readonly query: (
      sql: string,
    ) => Effect.Effect<unknown, never, Config | Logger>
  }
>()("Database") {}

Database.key // => "Database"

这里,Database service 的 query 函数同时需要 ConfigLogger。这种设计泄漏了实现细节,使 Database service 意识到自己的依赖,从而让测试变得复杂、难以 mock。

Keep Service Interfaces Simple

Service 函数应避免直接声明依赖。在实践中,service 操作的 Requirements 参数应设置为 never

                         ┌─── No dependencies required

Effect<Success, Error, never>

为演示这一问题,我们来创建一个 Database service 的测试实例:

示例(创建带有泄漏依赖的测试实例)

import { Effect, Context } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<Config, {}>()("Config") {}

// Declaring a service key for the Logger service
class Logger extends Context.Service<Logger, {}>()("Logger") {}

// Declaring a service key for the Database service
class Database extends Context.Service<
  Database,
  {
    readonly query: (
      sql: string,
    ) => Effect.Effect<unknown, never, Config | Logger>
  }
>()("Database") {}

// Declaring a test instance of the Database service
const DatabaseTest = Database.of({
  // Simulating a simple response
  query: (sql: string) => Effect.succeed([]),
})

import * as assert from "node:assert"

// A test that uses the Database service
const test = Effect.gen(function* () {
  const database = yield* Database
  const result = yield* database.query("SELECT * FROM users")
  assert.deepStrictEqual(result, [])
})

//      ┌─── Effect<unknown, never, Config | Logger>
//      ▼
const incompleteTestSetup = test.pipe(
  // Attempt to provide only the Database service without Config and Logger
  Effect.provideService(Database, DatabaseTest),
)

Database.key // => "Database"

由于 Database service 接口直接包含对 ConfigLogger 的依赖,任何测试准备工作都被迫包含这些 service,即使它们与测试无关。这带来了不必要的复杂度,也让编写简单、隔离的单元测试变得困难。

与其把依赖直接绑定到 Database service 接口上,不如在构造阶段管理依赖。

我们可以使用 Layer 来正确构造 Database service 并管理其依赖,而不会把细节泄漏到接口中。

Use Layers for Dependencies

当 service 自身带有需求时,最好把实现细节分离到 Layer 中。Layer 充当创建该 service 的构造器,让我们能够在构造层面而非 service 层面处理依赖。

创建 Layer

Layer 类型的结构如下:

        ┌─── The service to be created
        │                ┌─── The possible error
        │                │      ┌─── The required dependencies
        ▼                ▼      ▼
Layer<RequirementsOut, Error, RequirementsIn>

Layer 表示构造 RequirementsOut(即 service)的蓝图。它以 RequirementsIn(依赖)作为输入,并可能在构造过程中产生 Error 类型的错误。

参数描述
RequirementsOut要创建的 service 或资源。
Error构造 service 时可能发生的错误类型。
RequirementsIn构造 service 所需的依赖。

通过使用 Layer,你可以更好地组织 service,确保其依赖被清晰定义,并与实现细节分离。

为简单起见,我们假设值构造过程中不会遇到任何错误(即 Error = never)。

现在,让我们确定实现依赖图需要多少个 Layer:

Layer依赖类型
ConfigLiveConfig service 不依赖任何其他 serviceLayer<Config>
LoggerLiveLogger service 依赖 Config serviceLayer<Logger, never, Config>
DatabaseLiveDatabase service 依赖 ConfigLoggerLayer<Database, never, Config | Logger>
Naming Conventions

为某个 service 的 Layer 命名时,一个常见约定是:为 “live” 实现添加 Live 后缀,为 “test” 实现添加 Test 后缀。例如,对于 Database service,DatabaseLive 是你在应用中提供的 Layer,而 DatabaseTest 是你在测试中提供的 Layer。

当一个 service 有多个依赖时,它们表示为联合类型。在我们的例子中,Database service 同时依赖 ConfigLogger service。因此,DatabaseLive Layer 的类型为:

Layer<Database, never, Config | Logger>

Config

Config service 不依赖任何其他 service,因此 ConfigLive 是最容易实现的 Layer。正如管理 Service页面中那样,我们必须为该 service 创建一个 service key。由于该 service 没有依赖,我们可以直接使用 Layer.succeed 构造器创建 Layer:

import { Effect, Context, Layer } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

// Layer<Config, never, never>
const ConfigLive = Layer.succeed(
  Config,
  Config.of({
    getConfig: Effect.succeed({
      logLevel: "INFO",
      connection: "mysql://username:password@hostname:port/database_name",
    }),
  }),
)

await Effect.runPromise(
  Effect.provide(Config.pipe(Effect.andThen((c) => c.getConfig)), ConfigLive),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }

观察 ConfigLive 的类型,我们可以发现:

  • RequirementsOutConfig,表明构造该 Layer 将产出 Config service
  • Errornever,表明 Layer 构造不会失败
  • RequirementsInnever,表明该 Layer 没有依赖

注意,为了构造 ConfigLive,我们使用了 Config.of 构造器。然而,这只是一个用于确保实现具有正确类型推断的辅助方法。也可以跳过这个辅助方法,直接把实现构造成一个简单对象:

import { Effect, Context, Layer } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "mysql://username:password@hostname:port/database_name",
  }),
})

await Effect.runPromise(
  Effect.provide(Config.pipe(Effect.andThen((c) => c.getConfig)), ConfigLive),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }

Logger

现在我们继续实现 Logger service,它依赖 Config service 来获取一些配置。

正如我们在管理 Service页面中所做的那样,我们可以 yield Config service key,从 Context 中“提取”该 service。

由于使用 Config service key 是一个带 Effect 的操作,我们使用 Layer.effect 从生成的 Effect 创建 Layer。

import { Effect, Context, Layer } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "mysql://username:password@hostname:port/database_name",
  }),
})

// Declaring a service key for the Logger service
class Logger extends Context.Service<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}

// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (message) =>
        Effect.gen(function* () {
          const { logLevel } = yield* config.getConfig
          console.log(`[${logLevel}] ${message}`)
        }),
    }
  }),
)

await Effect.runPromise(
  Effect.provide(
    Logger.pipe(Effect.andThen((logger) => logger.log("hello"))),
    Layer.provide(LoggerLive, ConfigLive),
  ),
) // => undefined

观察 LoggerLive 的类型:

Layer<Logger, never, Config>

我们可以发现:

  • RequirementsOutLogger
  • Errornever,表明 Layer 构造不会失败
  • RequirementsInConfig,表明该 Layer 有一个需求

Database

最后,我们可以使用 ConfigLogger service 来实现 Database service。

import { Effect, Context, Layer } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "mysql://username:password@hostname:port/database_name",
  }),
})

// Declaring a service key for the Logger service
class Logger extends Context.Service<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}

// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (message) =>
        Effect.gen(function* () {
          const { logLevel } = yield* config.getConfig
          console.log(`[${logLevel}] ${message}`)
        }),
    }
  }),
)

// Declaring a service key for the Database service
class Database extends Context.Service<
  Database,
  { readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}

// Layer<Database, never, Config | Logger>
const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    return {
      query: (sql: string) =>
        Effect.gen(function* () {
          yield* logger.log(`Executing query: ${sql}`)
          const { connection } = yield* config.getConfig
          return { result: `Results from ${connection}` }
        }),
    }
  }),
)

const AppConfigLive = Layer.merge(ConfigLive, LoggerLive).pipe(
  Layer.provide(ConfigLive),
)

await Effect.runPromise(
  Effect.provide(
    Database.pipe(Effect.andThen((db) => db.query("SELECT * FROM users"))),
    Layer.provide(DatabaseLive, AppConfigLive),
  ),
) // => { result: "Results from mysql://username:password@hostname:port/database_name" }

观察 DatabaseLive 的类型:

Layer<Database, never, Config | Logger>

我们可以发现 RequirementsIn 类型是 Config | Logger,也就是说 Database service 同时需要 ConfigLogger service。

组合 Layer

Layer 可以通过两种主要方式组合:合并(merging)组合(composing)

合并 Layer

Layer 可以通过 Layer.merge 函数进行合并:

import { Layer } from "effect"

declare const layer1: Layer.Layer<"Out1", never, "In1">
declare const layer2: Layer.Layer<"Out2", never, "In2">

// Layer<"Out1" | "Out2", never, "In1" | "In2">
const merging = Layer.merge(layer1, layer2)

当我们合并两个 Layer 时,得到的 Layer:

  • 需要它们两者所需的所有 service("In1" | "In2")。
  • 产出它们两者产出的所有 service("Out1" | "Out2")。

例如,在上面的 Web 应用中,我们可以把 ConfigLiveLoggerLive 合并成单个 AppConfigLive Layer,它保留两个 Layer 的需求(never | Config = Config)以及两个 Layer 的输出(Config | Logger):

import { Effect, Context, Layer } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "mysql://username:password@hostname:port/database_name",
  }),
})

// Declaring a service key for the Logger service
class Logger extends Context.Service<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}

// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (message) =>
        Effect.gen(function* () {
          const { logLevel } = yield* config.getConfig
          console.log(`[${logLevel}] ${message}`)
        }),
    }
  }),
)

// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)

await Effect.runPromise(
  Effect.provide(
    Config.pipe(Effect.andThen((c) => c.getConfig)),
    AppConfigLive.pipe(Layer.provide(ConfigLive)),
  ),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }

组合 Layer

Layer 可以使用 Layer.provide 函数进行组合:

import { Layer } from "effect"

declare const inner: Layer.Layer<"OutInner", never, "InInner">
declare const outer: Layer.Layer<"InInner", never, "InOuter">

// Layer<"OutInner", never, "InOuter">
const composition = Layer.provide(inner, outer)

Layer 的顺序组合意味着一个 Layer 的输出被作为内层 Layer 的输入提供,结果得到单个 Layer:它拥有外层 Layer 的需求和内层 Layer 的输出。

现在我们可以把 AppConfigLive Layer 与 DatabaseLive Layer 组合起来:

import { Effect, Context, Layer } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "mysql://username:password@hostname:port/database_name",
  }),
})

// Declaring a service key for the Logger service
class Logger extends Context.Service<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}

// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (message) =>
        Effect.gen(function* () {
          const { logLevel } = yield* config.getConfig
          console.log(`[${logLevel}] ${message}`)
        }),
    }
  }),
)

// Declaring a service key for the Database service
class Database extends Context.Service<
  Database,
  { readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}

// Layer<Database, never, Config | Logger>
const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    return {
      query: (sql: string) =>
        Effect.gen(function* () {
          yield* logger.log(`Executing query: ${sql}`)
          const { connection } = yield* config.getConfig
          return { result: `Results from ${connection}` }
        }),
    }
  }),
)

// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)

// Layer<Database, never, never>
const MainLive = DatabaseLive.pipe(
  // provides the config and logger to the database
  Layer.provide(AppConfigLive),
  // provides the config to AppConfigLive
  Layer.provide(ConfigLive),
)

await Effect.runPromise(
  Effect.provide(
    Database.pipe(Effect.andThen((db) => db.query("SELECT * FROM users"))),
    MainLive,
  ),
) // => { result: "Results from mysql://username:password@hostname:port/database_name" }

我们得到了一个产出 Database service 的 MainLive Layer:

Layer<Database, never, never>

该 Layer 是我们应用完全解析后的 Layer。

合并与组合 Layer

假设我们希望 MainLive Layer 同时返回 ConfigDatabase service。可以通过 Layer.provideMerge 实现:

import { Effect, Context, Layer } from "effect"

// Declaring a service key for the Config service
class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "mysql://username:password@hostname:port/database_name",
  }),
})

// Declaring a service key for the Logger service
class Logger extends Context.Service<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}

const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (message) =>
        Effect.gen(function* () {
          const { logLevel } = yield* config.getConfig
          console.log(`[${logLevel}] ${message}`)
        }),
    }
  }),
)

// Declaring a service key for the Database service
class Database extends Context.Service<
  Database,
  { readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}

const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    return {
      query: (sql: string) =>
        Effect.gen(function* () {
          yield* logger.log(`Executing query: ${sql}`)
          const { connection } = yield* config.getConfig
          return { result: `Results from ${connection}` }
        }),
    }
  }),
)

// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)

// Layer<Config | Database, never, never>
const MainLive = DatabaseLive.pipe(
  Layer.provide(AppConfigLive),
  Layer.provideMerge(ConfigLive),
)

await Effect.runPromise(
  Effect.provide(
    Effect.gen(function* () {
      const config = yield* Config
      const database = yield* Database
      const queryResult = yield* database.query("SELECT * FROM users")
      return { logLevel: (yield* config.getConfig).logLevel, queryResult }
    }),
    MainLive,
  ),
) // => { logLevel: "INFO", queryResult: { result: "Results from mysql://username:password@hostname:port/database_name" } }

为 Effect 提供 Layer

现在我们已经为应用组装好了完全解析的 MainLive,可以使用 Effect.provide 将它提供给程序,以满足程序的需求:

import { Effect, Context, Layer } from "effect"

class Config extends Context.Service<
  Config,
  {
    readonly getConfig: Effect.Effect<{
      readonly logLevel: string
      readonly connection: string
    }>
  }
>()("Config") {}

const ConfigLive = Layer.succeed(Config, {
  getConfig: Effect.succeed({
    logLevel: "INFO",
    connection: "mysql://username:password@hostname:port/database_name",
  }),
})

class Logger extends Context.Service<
  Logger,
  { readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}

const LoggerLive = Layer.effect(
  Logger,
  Effect.gen(function* () {
    const config = yield* Config
    return {
      log: (message) =>
        Effect.gen(function* () {
          const { logLevel } = yield* config.getConfig
          console.log(`[${logLevel}] ${message}`)
        }),
    }
  }),
)

class Database extends Context.Service<
  Database,
  { readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}

const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config
    const logger = yield* Logger
    return {
      query: (sql: string) =>
        Effect.gen(function* () {
          yield* logger.log(`Executing query: ${sql}`)
          const { connection } = yield* config.getConfig
          return { result: `Results from ${connection}` }
        }),
    }
  }),
)

const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)

const MainLive = DatabaseLive.pipe(
  Layer.provide(AppConfigLive),
  Layer.provide(ConfigLive),
)

//      ┌─── Effect<unknown, never, Database>
//      ▼
const program = Effect.gen(function* () {
  const database = yield* Database
  const result = yield* database.query("SELECT * FROM users")
  return result
})

//      ┌─── Effect<unknown, never, never>
//      ▼
const runnable = Effect.provide(program, MainLive)

await Effect.runPromise(runnable) // => { result: "Results from mysql://username:password@hostname:port/database_name" }
/*
Output:
[INFO] Executing query: SELECT * FROM users
*/

注意 runnable 的需求类型是 never,表明该程序运行时不需要任何额外的 service。

把 Layer 转换为 Effect

有时你的整个应用可能就是一个 Layer,例如一个 HTTP server。你可以用 Layer.launch 把该 Layer 转换为 Effect。它会构造 Layer 并使其保持存活,直到被中断。

示例(启动一个 HTTP Server Layer)

import { Console, Context, Effect, Layer } from "effect"

class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}

// Simulating an HTTP server
const server = Layer.effect(
  HTTPServer,
  // Log a message to simulate a server starting
  Console.log("Listening on http://localhost:3000"),
)

// Converts the layer to an effect and runs it
Effect.runFork(Layer.launch(server))
/*
Output:
Listening on http://localhost:3000
...
*/

// Layer.launch never completes on its own; building the layer directly
// lets us verify what it produces without hanging the process
const context = await Effect.runPromise(Effect.scoped(Layer.build(server)))
Context.get(context, HTTPServer) // => undefined

Tap 操作

Layer.tapLayer.tapError 函数允许你根据 Layer 的成功或失败执行额外的 Effect。这些操作不会修改 Layer 的签名,但在 Layer 构造期间用于日志记录或执行副作用非常有用。

  • Layer.tap:当 Layer 成功获取时执行指定的 Effect。
  • Layer.tapError:当 Layer 获取失败时执行指定的 Effect。

示例(记录 Layer 获取过程中的成功与失败)

import { Config, Context, Effect, Layer, Console } from "effect"

class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}

// Simulating an HTTP server
const server = Layer.effect(
  HTTPServer,
  Effect.gen(function* () {
    const host = yield* Config.String("HOST")
    console.log(`Listening on http://localhost:${host}`)
  }),
).pipe(
  // Log a message if the layer acquisition succeeds
  Layer.tap((ctx) => Console.log(`layer acquisition succeeded with:\n${ctx}`)),
  // Log a message if the layer acquisition fails
  Layer.tapError((err) =>
    Console.log(`layer acquisition failed with:\n${err}`),
  ),
)

Effect.runFork(Layer.launch(server))
/*
Output:
layer acquisition failed with:
(Missing data at HOST: "Expected HOST to exist in the process context")
*/

错误处理

在构造 Layer 时,处理潜在错误很重要。Layer.catch 可以检查获取错误并返回一个回退 Layer。

catch

Layer.catch 函数允许你通过指定回退 Layer 从 Layer 构造期间的错误中恢复。这对于处理特定错误情况、确保应用能以替代方案继续运行很有用。

示例(从 Layer 构造期间的错误中恢复)

import { Config, Context, Effect, Layer } from "effect"

class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}

// Simulating an HTTP server
const server = Layer.effect(
  HTTPServer,
  Effect.gen(function* () {
    const host = yield* Config.String("HOST")
    console.log(`Listening on http://localhost:${host}`)
  }),
).pipe(
  // Recover from errors during layer construction
  Layer.catch((configError) =>
    Layer.effect(
      HTTPServer,
      Effect.gen(function* () {
        console.log(`Recovering from error:\n${configError}`)
        console.log(`Listening on http://localhost:3000`)
      }),
    ),
  ),
)

Effect.runFork(Layer.launch(server))
/*
Output:
Recovering from error:
(Missing data at HOST: "Expected HOST to exist in the process context")
Listening on http://localhost:3000
...
*/

如果回退逻辑不需要该错误,可以忽略传给 Layer.catch 的参数。

示例(回退到替代 Layer)

import { Config, Context, Effect, Layer } from "effect"

class Database extends Context.Service<Database, void>()("Database") {}

// Simulating a database connection
const postgresDatabaseLayer = Layer.effect(
  Database,
  Effect.gen(function* () {
    const databaseConnectionString = yield* Config.String("CONNECTION_STRING")
    console.log(`Connecting to database with: ${databaseConnectionString}`)
  }),
)

// Simulating an in-memory database connection
const inMemoryDatabaseLayer = Layer.effect(
  Database,
  Effect.gen(function* () {
    console.log(`Connecting to in-memory database`)
  }),
)

// Fallback to in-memory database if PostgreSQL connection fails
const database = postgresDatabaseLayer.pipe(
  Layer.catch(() => inMemoryDatabaseLayer),
)

Effect.runFork(Layer.launch(database))
/*
Output:
Connecting to in-memory database
...
*/

使用 Context.Service 定义 Service

Context.Service 可以把 service key 及其构造 Effect 一起定义为一个类。之后你可以把 Layer 作为该类的静态字段暴露出来。

定义带依赖的 Service

下面的例子定义了一个依赖文件系统的 Cache service。

示例(定义 Cache Service)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer } from "effect"

// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
  // Define how to create the service
  make: Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem
    const lookup = (key: string) => fs.readFileString(`cache/${key}`)
    return { lookup } as const
  }),
}) {
  // Specify dependencies
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(NodeFileSystem.layer),
  )
}

Cache.key // => "app/Cache"

声明 Service 的 Layer

把 service 的 Layer 声明为静态类字段,通过 Layer.effectthis.make 构建。下面的字段名只是约定,并非 API 的一部分。

静态字段描述
Cache.layer提供 Cache service,并已包含其依赖。
Cache.layerWithoutDependencies提供 Cache service,但需要单独提供依赖。
import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer } from "effect"

// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
  make: Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem
    const lookup = (key: string) => fs.readFileString(`cache/${key}`)
    return { lookup } as const
  }),
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(NodeFileSystem.layer),
  )
  static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}

// Layer that includes all required dependencies
//
//      ┌─── Layer<Cache>
//      ▼
const layer = Cache.layer

// Layer without dependencies, requiring them to be provided externally
//
//      ┌─── Layer.Layer<Cache, never, FileSystem>
//      ▼
const layerNoDeps = Cache.layerWithoutDependencies

// Exercise layerNoDeps with a test FileSystem to confirm the wiring works
const FileSystemTest = FileSystem.layerNoop({
  readFileString: () => Effect.succeed("File Content..."),
})

await Effect.runPromise(
  Effect.provide(
    Effect.gen(function* () {
      const cache = yield* Cache
      return yield* cache.lookup("my-key")
    }),
    Layer.provide(layerNoDeps, FileSystemTest),
  ),
) // => "File Content..."

访问 Service

使用 Context.Service 创建的 service 可以像其他任何 Effect service 一样被访问。

示例(访问 Cache Service)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"

// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
  make: Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem
    const lookup = (key: string) => fs.readFileString(`cache/${key}`)
    return { lookup } as const
  }),
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(NodeFileSystem.layer),
  )
  static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}

// Accessing the Cache Service
const program = Effect.gen(function* () {
  const cache = yield* Cache
  const data = yield* cache.lookup("my-key")
  console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))

const runnable = program.pipe(Effect.provide(Cache.layer))

Effect.runFork(runnable)
/*
{
  _id: 'Cause',
  failures: [
    {
      _tag: 'Fail',
      error: {
        _tag: 'PlatformError',
        reason: {
          _tag: 'NotFound',
          module: 'FileSystem',
          method: 'readFile',
          pathOrDescriptor: 'cache/my-key',
          syscall: 'open',
          cause: [Error: ENOENT: no such file or directory, open 'cache/my-key']
        }
      }
    }
  ]
}
*/

由于该示例使用 Cache.layer,它会与真实文件系统交互。如果文件不存在,就会产生错误。

注入测试依赖

为了在不依赖真实文件系统的情况下测试程序,我们可以使用 Cache.layerWithoutDependencies Layer 注入一个测试文件系统。

示例(使用测试文件系统)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"

// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
  make: Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem
    const lookup = (key: string) => fs.readFileString(`cache/${key}`)
    return { lookup } as const
  }),
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(NodeFileSystem.layer),
  )
  static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}

// Accessing the Cache Service
const program = Effect.gen(function* () {
  const cache = yield* Cache
  const data = yield* cache.lookup("my-key")
  console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))

// Create a test file system that always returns a fixed value
const FileSystemTest = FileSystem.layerNoop({
  readFileString: () => Effect.succeed("File Content..."),
})

const runnable = program.pipe(
  Effect.provide(Cache.layerWithoutDependencies),
  // Provide the mock file system
  Effect.provide(FileSystemTest),
)

Effect.runFork(runnable)
// Output: File Content...

await Effect.runPromise(runnable) // => undefined

直接 Mock Service

另一种方式是不替换依赖,而是直接 mock Cache service 本身。

示例(Mock Cache Service)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"

// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
  make: Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem
    const lookup = (key: string) => fs.readFileString(`cache/${key}`)
    return { lookup } as const
  }),
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(NodeFileSystem.layer),
  )
  static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}

// Accessing the Cache Service
const program = Effect.gen(function* () {
  const cache = yield* Cache
  const data = yield* cache.lookup("my-key")
  console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))

// Create a mock implementation of Cache
const cache = Cache.of({
  lookup: () => Effect.succeed("Cache Content..."),
})

// Provide the mock Cache service
const runnable = program.pipe(Effect.provideService(Cache, cache))

Effect.runFork(runnable)
// Output: Cache Content...

await Effect.runPromise(runnable) // => undefined

构造 make 的其他方式

Context.Servicemake 字段接受任何 Effect,因此 service 可以使用不同的构造风格:

风格如何构建 make
静态值Effect.succeed(...):一个常量实现。
同步Effect.sync(() => ...):一个同步构造器。
EffectfulEffect.gen(function* () { ... })(或任何其他 Effect):一个自身依赖其他 service 的构造器。
Scopedmake 内使用 Effect.acquireRelease/Effect.addFinalizerEffect.gen(...):生命周期管理,无需单独的选项。

示例(定义具有静态实现的 Service)

这是定义 service 最简单的方式。当你希望为 service 提供一个常量值时,它很有用。

import { Context, Effect, Layer } from "effect"

class MagicNumber extends Context.Service<MagicNumber>()("MagicNumber", {
  make: Effect.succeed({ value: 42 }),
}) {
  static readonly layer = Layer.effect(this, this.make)
}

//      ┌─── Effect<void, never, MagicNumber>
//      ▼
const program = Effect.gen(function* () {
  const magicNumber = yield* MagicNumber
  console.log(`The magic number is ${magicNumber.value}`)
})

await Effect.runPromise(program.pipe(Effect.provide(MagicNumber.layer))) // => undefined
// The magic number is 42

示例(定义具有同步构造器的 Service)

import { Context, Effect, Layer, Random } from "effect"

class Sync extends Context.Service<Sync>()("Sync", {
  make: Effect.sync(() => ({
    next: Random.nextInt,
  })),
}) {
  static readonly layer = Layer.effect(this, this.make)
}

//      ┌─── Effect<void, never, Sync>
//      ▼
const program = Effect.gen(function* () {
  const sync = yield* Sync
  const n = yield* sync.next
  console.log(`The number is ${n}`)
})

await Effect.runPromise(program.pipe(Effect.provide(Sync.layer))) // => undefined
// Example Output: The number is 3858843290019673

示例(定义具有生命周期控制的 Service)

import { Context, Effect, Layer, Console } from "effect"

class Scoped extends Context.Service<Scoped>()("Scoped", {
  make: Effect.gen(function* () {
    // Acquire the resource and ensure it is properly released
    const resource = yield* Effect.acquireRelease(
      Console.log("Acquiring...").pipe(Effect.as("foo")),
      () => Console.log("Releasing..."),
    )
    // Register a finalizer to run when the effect is completed
    yield* Effect.addFinalizer(() => Console.log("Shutting down"))
    return { resource }
  }),
}) {
  static readonly layer = Layer.effect(this, this.make)
}

//      ┌─── Effect<void, never, Scoped>
//      ▼
const program = Effect.gen(function* () {
  const resource = (yield* Scoped).resource
  console.log(`The resource is ${resource}`)
})

await Effect.runPromise(
  program.pipe(
    Effect.provide(
      //       ┌─── Layer<Scoped, never, never>
      //       ▼
      Scoped.layer,
    ),
  ),
) // => undefined
/*
Acquiring...
The resource is foo
Shutting down
Releasing...
*/

Scoped.layer Layer 不需要 Scope 作为依赖,因为 Scoped 自身管理其生命周期。