Effect Курс Доступ к сервисам

Доступ к сервисам

Способы доступа к сервисам в Effect.

Теория

Способы доступа к сервисам

Effect предоставляет несколько механизмов для получения сервисов из Context. Каждый имеет своё предназначение:

МетодВозвращаетКогда использовать
yield* TagСервис напрямуюСтандартный способ в generators
Effect.serviceEffect с сервисомАльтернатива для pipe-стиля
Effect.serviceOptionEffect с OptionОпциональные зависимости
Effect.serviceFunctionEffect с результатом функцииОднострочный доступ + трансформация
Effect.serviceFunctionEffectEffect с вложенным EffectКогда функция возвращает Effect

Базовый доступ через yield*

Самый идиоматичный способ — использование yield* в generator:


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

const program = Effect.gen(function* () {
  // yield* Tag — это идиоматичный способ доступа
  const logger = yield* Logger
  
  yield* logger.log("Hello!")
})

📖 Как это работает:

Tag реализует Symbol.iterator, что позволяет использовать его с yield*:

// Внутри Tag определено:
class Tag<Id, Service> {
  readonly [Symbol.iterator] = () => {
    // Возвращает generator, который при yield* вернёт Service
    return Effect.service(this)[Symbol.iterator]()
  }
}

// Поэтому эти записи эквивалентны:
const v1 = yield* Logger                    // Короткий синтаксис
const v2 = yield* Effect.service(Logger)    // Полный синтаксис

Effect.serviceConstants: явный доступ

Effect.serviceConstants — функция, которая создаёт сервис значения которого обёрнуты в Effect с зависимастями от данного сервиса :


class Database extends Context.Tag("Database")<
  Database,
  {
    readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
    readonly url: string
  }
>() {}
const getDatabase = Effect.serviceConstants(Database)
//     ^? {
//          query: Effect.Effect<(sql: string) => Effect.Effect<ReadonlyArray<unknown>>, never, Database>
//          url: Effect.Effect<string, never, Database>
//        }

const program = getDatabase.query.pipe(
  Effect.flatMap((query) => query("SELECT * FROM users")),
  Effect.tap((h) => Effect.log(`Found ${h.length} users`)),
)

// Эквивалент в generator-стиле
const programGen = Effect.gen(function* () {
  const db = yield* Database // или yield* Effect.service(Database)
  const users = yield* db.query("SELECT * FROM users")
  yield* Effect.log(`Found ${users.length} users`)
  return users
})

💡 Когда использовать Effect.serviceConstants:

  • В pipe-based коде
  • Когда нужен Effect для композиции
  • В тестах для проверки типов

Effect.serviceOption: опциональные зависимости

Иногда сервис может отсутствовать, и программа должна это обработать:


class Analytics extends Context.Tag("Analytics")<
  Analytics,
  { readonly track: (event: string) => Effect.Effect<void> }
>() {}

// serviceOption возвращает Option<Service>
// ВАЖНО: Requirements остаётся never, а не Analytics!
const maybeTrack = (event: string): Effect.Effect<void, never, never> =>
  Effect.gen(function* () {
    const maybeAnalytics = yield* Effect.serviceOption(Analytics)
    
    if (Option.isSome(maybeAnalytics)) {
      yield* maybeAnalytics.value.track(event)
    } else {
      yield* Effect.log(`Analytics not available, skipping: ${event}`)
    }
  })

📊 Сравнение обязательного и опционального доступа:

// Обязательный — программа НЕ запустится без сервиса
const required = Effect.gen(function* () {
  const logger = yield* Logger  // Requirements: Logger
  yield* logger.log("hello")
})
// Тип: Effect<void, never, Logger>

// Опциональный — программа запустится в любом случае
const optional = Effect.gen(function* () {
  const maybeLogger = yield* Effect.serviceOption(Logger)  // Requirements: never
  if (Option.isSome(maybeLogger)) {
    yield* maybeLogger.value.log("hello")
  }
})
// Тип: Effect<void, never, never>

⚠️ Важно: serviceOption не добавляет сервис в Requirements! Это позволяет программе работать независимо от наличия сервиса.


Effect.serviceFunction

Создаёт функцию, которая использует сервис из контекста для создания значения


class Config extends Context.Tag("Config")<
  Config,
  {
    readonly apiUrl: string
    readonly timeout: number
    readonly features: ReadonlySet<string>
  }
>() {}

// serviceFunction = service + map в одном вызове
const getApiUrl = Effect.serviceFunction(Config, (config) => () => config.apiUrl)

const getTimeout = Effect.serviceFunction(Config, (config) => () => config.timeout)

const hasFeature = Effect.serviceFunction(
  Config,
  (config) => (feature: string) => config.features.has(feature),
)

// Эквивалент через gen:
const getApiUrlGen = Effect.gen(function* () {
  const config = yield* Config
  return config.apiUrl
})

// Использование
const program = Effect.gen(function* () {
  const url = yield* getApiUrl()
  const timeout = yield* getTimeout()
  const hasDarkMode = yield* hasFeature("dark-mode")

  console.log(`API: ${url}, timeout: ${timeout}, darkMode: ${hasDarkMode}`)
})

💡 Когда использовать serviceFunction:

  • Когда нужно только одно значение из сервиса
  • Для создания переиспользуемых функций
  • Для упрощения доступа к конфигурации

Effect.serviceFunctionEffect

Создаёт функцию, которая использует сервис из контекста для создания эффекта.


class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findById: (id: string) => Effect.Effect<User | null, DatabaseError>
    readonly findByEmail: (email: string) => Effect.Effect<User | null, DatabaseError>
  }
>() {}

// serviceFunctionEffect = service + flatMap в одном вызове
const getUserById = Effect.serviceFunctionEffect(UserRepository, (repo) => repo.findById)
const printUserId = Effect.serviceFunctionEffect(
  UserRepository,
  (repo) => (id: string) => repo.findById(id).pipe(Effect.map((h) => `UserId: ${h.id}`)),
)


const getUserByEmail = Effect.serviceFunctionEffect(UserRepository, (repo) => repo.findByEmail)

// Эквивалент через gen:
const getUserByIdGen = (id: string) =>
  Effect.gen(function* () {
    const repo = yield* UserRepository
    return yield* repo.findById(id)
  })

// Использование
const program = Effect.gen(function* () {
  const user = yield* getUserById("user-123")
  if (user) {
    console.log(`Found: ${user.name}`)
  }
})

Паттерн “Accessor Functions”

Распространённый паттерн — создание accessor-функций для удобного доступа:


// Определение сервиса
class Cache extends Context.Tag("Cache")<
  Cache,
  {
    readonly get: (key: string) => Effect.Effect<string | null>
    readonly set: (key: string, value: string, ttlSeconds?: number) => Effect.Effect<void>
    readonly delete: (key: string) => Effect.Effect<boolean>
    readonly clear: () => Effect.Effect<void>
  }
>() {}

// Accessor functions
const cacheGet =  
  Effect.serviceFunctionEffect(Cache, (cache) => cache.get)

const cacheSet = 
  Effect.serviceFunctionEffect(Cache, (cache) => (key: string, value: string, ttl?: number) => cache.set(key, value, ttl))

const cacheDelete = 
  Effect.serviceFunctionEffect(Cache, (cache) => cache.delete)

const cacheClear = 
  Effect.serviceFunctionEffect(Cache, (cache) => cache.clear)

// Теперь клиентский код чище:
const program = Effect.gen(function* () {
  yield* cacheSet("user:123", JSON.stringify({ name: "Alice" }), 3600)
  
  const cached = yield* cacheGet("user:123")
  if (cached) {
    console.log(`From cache: ${cached}`)
  }
  
  yield* cacheDelete("user:123")
})

Direct Method Access через Effect.Tag

Effect.Tag автоматически создаёт accessor-ы как статические свойства:


// Effect.Tag вместо Context.Tag
class EmailService extends Effect.Tag("EmailService")<
  EmailService,
  {
    readonly send: (to: string, subject: string, body: string) => Effect.Effect<void, EmailError>
    readonly sendBulk: (messages: ReadonlyArray<Email>) => Effect.Effect<void, EmailError>
  }
>() {}

// Методы доступны напрямую как статические свойства!
const sendWelcome = (email: string) => 
  EmailService.send(email, "Welcome!", "Welcome to our platform")
// Тип: Effect<void, EmailError, EmailService>

// Без Effect.Tag нужно было бы:
const sendWelcomeExplicit = (email: string) => Effect.gen(function* () {
  const service = yield* EmailService
  yield* service.send(email, "Welcome!", "Welcome to our platform")
})

⚠️ Ограничение: Direct access не работает с generic методами:

class Repository extends Effect.Tag("Repository")<
  Repository,
  {
    // Generic метод — НЕ будет доступен через Repository.find
    readonly find: <T>(table: string, id: string) => Effect.Effect<T | null>
    // Конкретный метод — БУДЕТ доступен через Repository.findUser
    readonly findUser: (id: string) => Effect.Effect<User | null>
  }
>() {}

// ✅ Работает
const user = Repository.findUser("123")

// ❌ НЕ работает — нужен явный доступ
// const item = Repository.find<Product>("products", "456")

// ✅ Нужно так:
const item = Effect.gen(function* () {
  const repo = yield* Repository
  return yield* repo.find<Product>("products", "456")
})

Условный доступ к сервисам

Иногда логика зависит от наличия или состояния сервиса:

Effect.serviceOption


class FeatureFlags extends Context.Tag("FeatureFlags")<
  FeatureFlags,
  {
    readonly isEnabled: (flag: string) => boolean
    readonly getVariant: (flag: string) => string | null
  }
>() {}

class Analytics extends Context.Tag("Analytics")<
  Analytics,
  { readonly track: (event: string, data?: object) => Effect.Effect<void> }
>() {}

// Условное выполнение на основе feature flag
const trackIfEnabled = (event: string, data?: object) => Effect.gen(function* () {
  const flags = yield* FeatureFlags
  
  if (flags.isEnabled("analytics")) {
    // Analytics обязателен только если флаг включён
    const analytics = yield* Analytics
    yield* analytics.track(event, data)
  }
})

// Проблема: Requirements = FeatureFlags | Analytics
// Даже если analytics отключён, нужно предоставить оба сервиса!

// Решение: использовать serviceOption для Analytics
const trackIfEnabledOptional = (event: string, data?: object) => Effect.gen(function* () {
  const flags = yield* FeatureFlags
  const maybeAnalytics = yield* Effect.serviceOption(Analytics)
  
  if (flags.isEnabled("analytics") && Option.isSome(maybeAnalytics)) {
    yield* maybeAnalytics.value.track(event, data)
  }
})
// Requirements = FeatureFlags (Analytics опционален)

Effect.serviceOptional

Получение опционального сервиса без использования Option. Если сервис отсутствует, он сдаёт NoSuchElementException, которые можно обрабатывать с помощью механизмов обработки ошибок Effect.

Обновление сервисов

Эта функция изменяет существующую реализацию сервиса в контексте. Он получает текущий сервис, применяет предоставленную функцию преобразования f, и заменяет старый сервис на преобразованный.

Это полезно для адаптации или расширения поведения сервиса во время выполнения эффекта.


class Data extends Context.Tag("Data")<
  Data,
  {
    readonly apiUrl: string
    readonly timeout: number
    readonly hasDarkMode: boolean
  }
>() {}

// Использование
const program = Effect.gen(function* () {
  const data = yield* Data

  console.log(`API: ${data.apiUrl}, timeout: ${data.timeout}, darkMode: ${data.hasDarkMode}`)
})

const runnable = Effect.updateService(Data, (h) => ({
  ...h,
  apiUrl: "https://api.example.com",
}))(program)

Концепция ФП: Accessing the Environment

Reader Monad ask

В классической Reader Monad есть операция ask для доступа к окружению:

// Reader<R, A> = (env: R) => A

// ask :: Reader<R, R>
const ask = <R>(): Reader<R, R> => (env) => env

// asks :: (R => A) => Reader<R, A>
const asks = <R, A>(f: (env: R) => A): Reader<R, A> => (env) => f(env)

// Использование
interface Env {
  readonly apiUrl: string
  readonly token: string
}

const getApiUrl: Reader<Env, string> = asks((env) => env.apiUrl)
const getToken: Reader<Env, string> = asks((env) => env.token)

Mapping к Effect API

ReaderEffectОписание
askyield* Tag / Effect.serviceПолучить весь сервис
asks(f)Effect.serviceFunction(Tag, f)Получить и трансформировать
local(f, reader)Effect.provideServiceИзменить окружение
// Reader-стиль
const reader: Reader<Config, string> = asks((c) => c.apiUrl)

// Effect-стиль
const effect: Effect.Effect<string, never, Config> = 
  Effect.serviceFunction(Config, (c) => () => c.apiUrl)

Has Pattern (исторический контекст)

В ранних версиях Effect использовался паттерн Has<T>:

// Старый стиль (до Effect 3.x)
type Has<T> = { readonly [tag]: T }
type Requirements = Has<Logger> & Has<Database>

// Новый стиль
type Requirements = Logger | Database  // Union типов

Текущий подход с union типами проще и лучше интегрируется с TypeScript.


API Reference

Effect.serviceConstants [STABLE]

Создаёт Effect, который при выполнении возвращает сервис из Context. Удобен для получения значений из сервиса, но для получения функций лучше использовать Effect.serviceFunction

declare const serviceConstants: <S, SE, SR>(
  getService: Effect<S, SE, SR>
) => {
  [k in { [k in keyof S]: k }[keyof S]]: S[k] extends Effect<infer A, infer E, infer R>
    ? Effect<A, SE | E, SR | R>
    : Effect<S[k], SE, SR>
}

Effect.serviceOption [STABLE]

Возвращает Option сервиса. НЕ добавляет сервис в Requirements.

declare const serviceOption: <I, S>(tag: Context.Tag<I, S>) => Effect<Option.Option<S>>

Effect.serviceFunction [STABLE]

Получает сервис и функцию с сервисом, которая возвращает функцию использования этого сервиса

declare const serviceFunction: <T extends Effect<any, any, any>, Args extends Array<any>, A>(
  getService: T,
  f: (_: Effect.Success<T>) => (...args: Args) => A
) => (...args: Args) => Effect<A, Effect.Error<T>, Effect.Context<T>>

Effect.serviceFunctionEffect [STABLE]

Получает сервис и применяет функцию, возвращающую Effect.

declare const serviceFunctionEffect: <T extends Effect<any, any, any>, Args extends Array<any>, A, E, R>(
  getService: T,
  f: (_: Effect.Success<T>) => (...args: Args) => Effect<A, E, R>
) => (...args: Args) => Effect<A, E | Effect.Error<T>, R | Effect.Context<T>>

Effect.updateService [STABLE]

Обновляет сервис в контексте новой реализацией

declare const updateService: {
  <I, S>(
    tag: Context.Tag<I, S>,
    f: (service: NoInfer<S>) => NoInfer<S>
  ): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, R | I>
  <A, E, R, I, S>(
    self: Effect<A, E, R>,
    tag: Context.Tag<I, S>,
    f: (service: NoInfer<S>) => NoInfer<S>
  ): Effect<A, E, R | I>
}

Примеры

Пример 1: Различные способы доступа


// Определение сервисов
class Config extends Context.Tag("Config")<
  Config,
  {
    readonly appName: string
    readonly version: string
    readonly debug: boolean
  }
>() {}

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

// 1. Базовый доступ через yield*
const example1 = Effect.gen(function* () {
  const config = yield* Config
  const logger = yield* Logger

  yield* logger.log(`Starting ${config.appName} v${config.version}`)

  if (config.debug) {
    yield* logger.debug("Debug mode enabled")
  }
})

// 2. Через Effect.serviceConstants (pipe-стиль)

const config = Effect.serviceConstants(Config)
const logger = Effect.serviceConstants(Logger)

const example2 = Effect.gen(function* () {
  const appName = yield* config.appName
  const consoleLog = yield* logger.log
  yield* consoleLog(`Starting ${appName} v${yield* config.version}`)
})

// 3. Через serviceFunction (для простых значений)
const getAppName = Effect.serviceFunction(Config, (c) => () => c.appName)
const getVersion = Effect.serviceFunction(Config, (c) => () => c.version)
const isDebug = Effect.serviceFunction(Config, (c) => () => c.debug)

const example3 = Effect.gen(function* () {
  const name = yield* getAppName()
  const version = yield* getVersion()
  console.log(`${name} v${version}`)
})

// 4. Через serviceFunctionEffect (для методов)
const logMessage = Effect.serviceFunctionEffect(Logger, (logger) => logger.log)

const example4 = logMessage("Hello from serviceFunctionEffect")

// 5. Опциональный доступ
const example5 = Effect.gen(function* () {
  const maybeLogger = yield* Effect.serviceOption(Logger)

  if (Option.isSome(maybeLogger)) {
    yield* maybeLogger.value.log("Logger is available")
  } else {
    console.log("Running without logger")
  }
})

Пример 2: Создание модуля с accessor-функциями


// === Типы и ошибки ===
interface User {
  readonly id: string
  readonly email: string
  readonly name: string
  readonly role: "user" | "admin"
}

class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
  readonly userId: string
}> {}

class DuplicateEmailError extends Data.TaggedError("DuplicateEmailError")<{
  readonly email: string
}> {}

// === Tag сервиса ===
interface UserServiceShape {
  readonly findById: (id: string) => Effect.Effect<User, UserNotFoundError>
  readonly findByEmail: (email: string) => Effect.Effect<User | null>
  readonly create: (data: Omit<User, "id">) => Effect.Effect<User, DuplicateEmailError>
  readonly update: (
    id: string,
    data: Partial<Omit<User, "id">>,
  ) => Effect.Effect<User, UserNotFoundError>
  readonly delete: (id: string) => Effect.Effect<void, UserNotFoundError>
  readonly listAll: () => Effect.Effect<ReadonlyArray<User>>
}

class UserService extends Context.Tag("UserService")<UserService, UserServiceShape>() {}

// === Accessor Functions ===
// Публичный API модуля — клиенты используют эти функции

export const findUserById = Effect.serviceFunctionEffect(UserService, (s) => s.findById)

export const findUserByEmail = Effect.serviceFunctionEffect(UserService, (s) => s.findByEmail)

export const createUser = Effect.serviceFunctionEffect(UserService, (s) => s.create)

export const updateUser = Effect.serviceFunctionEffect(UserService, (s) => s.update)

export const deleteUser = Effect.serviceFunctionEffect(UserService, (s) => s.delete)

export const listAllUsers = Effect.serviceFunctionEffect(UserService, (s) => s.listAll)

// === Клиентский код ===
const clientProgram = Effect.gen(function* () {
  // Чистый, декларативный код без прямого доступа к сервису
  const user = yield* createUser({
    email: "alice@example.com",
    name: "Alice",
    role: "user",
  })

  console.log(`Created user: ${user.id}`)

  const updatedUser = yield* updateUser(user.id, { role: "admin" })
  console.log(`Updated role to: ${updatedUser.role}`)

  const allUsers = yield* listAllUsers()
  console.log(`Total users: ${allUsers.length}`)
})

Пример 3: Опциональные зависимости для pluggable функциональности


// Обязательные сервисы
class Logger extends Context.Tag("Logger")<
  Logger,
  { readonly log: (msg: string) => Effect.Effect<void> }
>() {}

// Опциональные сервисы (plugins)
class Metrics extends Context.Tag("Metrics")<
  Metrics,
  { readonly increment: (name: string, value?: number) => Effect.Effect<void> }
>() {}

class Tracing extends Context.Tag("Tracing")<
  Tracing,
  { 
    readonly startSpan: (name: string) => Effect.Effect<Span>
    readonly endSpan: (span: Span) => Effect.Effect<void>
  }
>() {}

class Caching extends Context.Tag("Caching")<
  Caching,
  {
    readonly get: <T>(key: string) => Effect.Effect<T | null>
    readonly set: <T>(key: string, value: T, ttl?: number) => Effect.Effect<void>
  }
>() {}

// Программа с опциональными plugins
const processRequest = (requestId: string) => Effect.gen(function* () {
  const logger = yield* Logger  // Обязательно
  
  // Опциональные
  const maybeMetrics = yield* Effect.serviceOption(Metrics)
  const maybeTracing = yield* Effect.serviceOption(Tracing)
  const maybeCaching = yield* Effect.serviceOption(Caching)
  
  // Start tracing if available
  const span = Option.isSome(maybeTracing)
    ? yield* maybeTracing.value.startSpan(`request:${requestId}`)
    : null
  
  try {
    // Check cache if available
    if (Option.isSome(maybeCaching)) {
      const cached = yield* maybeCaching.value.get<string>(`response:${requestId}`)
      if (cached) {
        yield* logger.log(`Cache hit for ${requestId}`)
        if (Option.isSome(maybeMetrics)) {
          yield* maybeMetrics.value.increment("cache.hit")
        }
        return cached
      }
    }
    
    // Process request
    yield* logger.log(`Processing ${requestId}`)
    const result = `Result for ${requestId}`
    
    // Cache result if available
    if (Option.isSome(maybeCaching)) {
      yield* maybeCaching.value.set(`response:${requestId}`, result, 3600)
    }
    
    // Record metrics if available
    if (Option.isSome(maybeMetrics)) {
      yield* maybeMetrics.value.increment("request.processed")
    }
    
    return result
  } finally {
    // End span if tracing available
    if (span && Option.isSome(maybeTracing)) {
      yield* maybeTracing.value.endSpan(span)
    }
  }
})

// Тип: Effect<string, never, Logger>
// Только Logger обязателен!

// Минимальный запуск
const minimal = processRequest("req-1").pipe(
  Effect.provideService(Logger, { 
    log: (msg) => Effect.sync(() => console.log(msg)) 
  })
)

// С plugins
const withPlugins = processRequest("req-2").pipe(
  Effect.provideService(Logger, { log: (msg) => Effect.sync(() => console.log(msg)) }),
  Effect.provideService(Metrics, { increment: (n) => Effect.sync(() => console.log(`Metric: ${n}`)) }),
  Effect.provideService(Caching, {
    get: () => Effect.succeed(null),
    set: () => Effect.void
  })
)

Пример 4: Effect.Tag с прямым доступом


// Effect.Tag — методы становятся статическими
class Notifications extends Effect.Tag("Notifications")<
  Notifications,
  {
    readonly send: (userId: string, message: string) => Effect.Effect<void>
    readonly sendBulk: (userIds: ReadonlyArray<string>, message: string) => Effect.Effect<void>
    readonly getUnreadCount: (userId: string) => Effect.Effect<number>
  }
>() {}

// Использование через статические методы (прямой доступ)
const notifyUser = (userId: string, message: string) =>
  Notifications.send(userId, message)

const broadcastMessage = (message: string) =>
  Effect.gen(function* () {
    const userIds = ["user-1", "user-2", "user-3"]
    yield* Notifications.sendBulk(userIds, message)
  })

const checkNotifications = (userId: string) =>
  Notifications.getUnreadCount(userId).pipe(
    Effect.map((count) => count > 0 ? `You have ${count} unread` : "No new notifications")
  )

// Полная программа
const program = Effect.gen(function* () {
  yield* Notifications.send("user-1", "Welcome!")
  const unread = yield* Notifications.getUnreadCount("user-1")
  console.log(`Unread: ${unread}`)
})

// Layer для production
const NotificationsLive = Layer.succeed(Notifications, {
  send: (userId, message) => Effect.sync(() => 
    console.log(`[${userId}] ${message}`)
  ),
  sendBulk: (userIds, message) => Effect.sync(() => 
    userIds.forEach(id => console.log(`[${id}] ${message}`))
  ),
  getUnreadCount: (_userId) => Effect.succeed(5)
})

Effect.runPromise(program.pipe(Effect.provide(NotificationsLive)))

Пример 5: Селекторы конфигурации


// Сложная конфигурация
interface AppConfigInterface {
  readonly server: {
    readonly host: string
    readonly port: number
    readonly ssl: boolean
  }
  readonly database: {
    readonly url: string
    readonly poolSize: number
    readonly timeout: number
  }
  readonly features: {
    readonly darkMode: boolean
    readonly beta: boolean
    readonly maxUploadSize: number
  }
  readonly auth: {
    readonly jwtSecret: string
    readonly tokenTtl: number
    readonly refreshTokenTtl: number
  }
}

class AppConfig extends Context.Tag("AppConfig")<AppConfig, AppConfigInterface>() {}

// === Селекторы ===
// Группируем по доменам для удобства

// Server
const getServerHost = Effect.serviceFunction(AppConfig, (c) => () => c.server.host)
const getServerPort = Effect.serviceFunction(AppConfig, (c) => () => c.server.port)
const isSSLEnabled = Effect.serviceFunction(AppConfig, (c) => () => c.server.ssl)
const getServerUrl = Effect.serviceFunction(
  AppConfig,
  (c) => () => `${c.server.ssl ? "https" : "http"}://${c.server.host}:${c.server.port}`,
)

// Database
const getDatabaseUrl = Effect.serviceFunction(AppConfig, (c) => () => c.database.url)
const getPoolSize = Effect.serviceFunction(AppConfig, (c) => () => c.database.poolSize)
const getDbTimeout = Effect.serviceFunction(AppConfig, (c) => () => c.database.timeout)

// Features
const isDarkModeEnabled = Effect.serviceFunction(AppConfig, (c) => () => c.features.darkMode)
const isBetaEnabled = Effect.serviceFunction(AppConfig, (c) => () => c.features.beta)
const getMaxUploadSize = Effect.serviceFunction(AppConfig, (c) => () => c.features.maxUploadSize)

// Auth
const getJwtSecret = Effect.serviceFunction(AppConfig, (c) => () => c.auth.jwtSecret)
const getTokenTtl = Effect.serviceFunction(AppConfig, (c) => () => c.auth.tokenTtl)

// === Использование ===
const initializeServer = Effect.gen(function* () {
  const url = yield* getServerUrl()
  const poolSize = yield* getPoolSize()
  const beta = yield* isBetaEnabled()

  console.log(`Server: ${url}`)
  console.log(`DB Pool: ${poolSize}`)
  console.log(`Beta features: ${beta ? "enabled" : "disabled"}`)
})

Пример 6: Комбинирование обязательных и опциональных зависимостей


// Обязательный сервис
class Database extends Context.Tag("Database")<
  Database,
  { readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>> }
>() {}

// Опциональный сервис кэширования
class QueryCache extends Context.Tag("QueryCache")<
  QueryCache,
  {
    readonly get: (key: string) => Effect.Effect<ReadonlyArray<unknown> | null>
    readonly set: (key: string, value: ReadonlyArray<unknown>) => Effect.Effect<void>
  }
>() {}

// Функция с кэшированием, если доступно
const cachedQuery = (sql: string) => Effect.gen(function* () {
  const db = yield* Database  // Обязательно
  const maybeCache = yield* Effect.serviceOption(QueryCache)  // Опционально
  
  const cacheKey = `query:${sql}`
  
  // Пробуем получить из кэша
  if (Option.isSome(maybeCache)) {
    const cached = yield* maybeCache.value.get(cacheKey)
    if (cached) {
      console.log("Cache HIT")
      return cached
    }
    console.log("Cache MISS")
  }
  
  // Выполняем запрос
  const result = yield* db.query(sql)
  
  // Сохраняем в кэш, если доступен
  if (Option.isSome(maybeCache)) {
    yield* maybeCache.value.set(cacheKey, result)
  }
  
  return result
})

// Тип: Effect<ReadonlyArray<unknown>, never, Database>
// QueryCache не в Requirements!

// Запуск без кэша
const withoutCache = cachedQuery("SELECT * FROM users").pipe(
  Effect.provideService(Database, {
    query: (sql) => Effect.succeed([{ id: 1 }])
  })
)

// Запуск с кэшем
const withCache = cachedQuery("SELECT * FROM users").pipe(
  Effect.provideService(Database, {
    query: (sql) => Effect.succeed([{ id: 1 }])
  }),
  Effect.provideService(QueryCache, {
    get: (key) => Effect.succeed(null),
    set: (key, value) => Effect.sync(() => console.log(`Cached: ${key}`))
  })
)

Упражнения

Упражнение

Базовый доступ к сервису

Легко

Реализуйте программу, использующую сервис Random:


class Random extends Context.Tag("Random")<
  Random,
  {
    readonly nextInt: (max: number) => Effect.Effect<number>
    readonly nextBoolean: () => Effect.Effect<boolean>
  }
>() {}

// Задание: напишите программу, которая:
// 1. Генерирует случайное число от 0 до 100
// 2. Генерирует случайный boolean
// 3. Возвращает строку вида "Number: X, Boolean: Y"

const program: Effect.Effect<string, never, Random> = // Ваш код
Упражнение

serviceFunction

Легко

Создайте селекторы для конфигурации:


class Settings extends Context.Tag("Settings")<
  Settings,
  {
    readonly theme: "light" | "dark"
    readonly language: string
    readonly notifications: boolean
    readonly fontSize: number
  }
>() {}

// Задание: создайте селекторы через serviceFunction
const getTheme: Effect.Effect<"light" | "dark", never, Settings> = // ...
const getLanguage: Effect.Effect<string, never, Settings> = // ...
const areNotificationsEnabled: Effect.Effect<boolean, never, Settings> = // ...
const getFontSize: Effect.Effect<number, never, Settings> = // ...
Упражнение

serviceOption

Легко

Реализуйте функцию с опциональной аналитикой:


class Analytics extends Context.Tag("Analytics")<
  Analytics,
  { readonly track: (event: string) => Effect.Effect<void> }
>() {}

// Задание: создайте функцию, которая:
// 1. Трекает событие, если Analytics доступен
// 2. Ничего не делает, если Analytics недоступен
// 3. Возвращает true если событие отправлено, false если нет
// Requirements НЕ должен включать Analytics!

const maybeTrack = (event: string): Effect.Effect<boolean, never, never> = // ...
Упражнение

Accessor модуль

Средне

Создайте полноценный модуль с accessor-функциями:


// Ошибки
class ProductNotFound extends Data.TaggedError("ProductNotFound")<{ id: string }> {}
class InsufficientStock extends Data.TaggedError("InsufficientStock")<{ id: string; available: number }> {}

// Tag
class Inventory extends Context.Tag("Inventory")<
  Inventory,
  {
    readonly getStock: (productId: string) => Effect.Effect<number, ProductNotFound>
    readonly reserve: (productId: string, quantity: number) => Effect.Effect<void, ProductNotFound | InsufficientStock>
    readonly release: (productId: string, quantity: number) => Effect.Effect<void, ProductNotFound>
  }
>() {}

// Задание: создайте accessor-функции
export const getProductStock = (productId: string) => // ...
export const reserveProduct = (productId: string, quantity: number) => // ...
export const releaseProduct = (productId: string, quantity: number) => // ...

// И функцию-хелпер, которая проверяет, есть ли товар в наличии
export const isInStock = (productId: string, minQuantity: number = 1): 
  Effect.Effect<boolean, ProductNotFound, Inventory> => // ...
Упражнение

Conditional service access

Средне

Реализуйте паттерн “use if available”:


class EncryptionService extends Context.Tag("Encryption")<
  EncryptionService,
  {
    readonly encrypt: (data: string) => Effect.Effect<string>
    readonly decrypt: (data: string) => Effect.Effect<string>
  }
>() {}

// Задание: создайте функции, которые:
// - Шифруют данные, если EncryptionService доступен
// - Возвращают исходные данные, если сервис недоступен

const encryptIfAvailable = (data: string): Effect.Effect<string, never, never> => // ...
const decryptIfAvailable = (data: string): Effect.Effect<string, never, never> => // ...
Упражнение

Composed accessors

Средне

Создайте составные accessor-ы:


class UserService extends Context.Tag("UserService")<
  UserService,
  { readonly findById: (id: string) => Effect.Effect<User | null> }
>() {}

class OrderService extends Context.Tag("OrderService")<
  OrderService,
  { readonly findByUserId: (userId: string) => Effect.Effect<ReadonlyArray<Order>> }
>() {}

class PaymentService extends Context.Tag("PaymentService")<
  PaymentService,
  { readonly getBalance: (userId: string) => Effect.Effect<number> }
>() {}

// Задание: создайте составной accessor, который возвращает полный профиль пользователя
interface UserProfile {
  readonly user: User
  readonly orders: ReadonlyArray<Order>
  readonly balance: number
}

const getUserProfile = (userId: string): 
  Effect.Effect<UserProfile | null, never, UserService | OrderService | PaymentService> => // ...
Упражнение

Generic accessor factory

Сложно

Создайте фабрику для accessor-функций:


// Задание: создайте generic функцию, которая генерирует accessor-ы
// для любого Tag и метода

type MethodOf<S, K extends keyof S> = S[K] extends (...args: infer A) => infer R 
  ? (...args: A) => R 
  : never

const createAccessor = <T extends Context.Tag<any, any>, K extends keyof Context.Tag.Service<T>>(
  tag: T,
  method: K
): MethodOf<Context.Tag.Service<T>, K> extends (...args: infer A) => Effect.Effect<infer R, infer E, infer R2>
  ? (...args: A) => Effect.Effect<R, E, R2 | Context.Tag.Identifier<T>>
  : never => {
  // Ваш код
}

// Использование:
class Database extends Context.Tag("Database")<
  Database,
  {
    readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
    readonly execute: (sql: string) => Effect.Effect<number>
  }
>() {}

const query = createAccessor(Database, "query")
const execute = createAccessor(Database, "execute")
Упражнение

Service aggregator

Сложно

Создайте паттерн для агрегации нескольких опциональных сервисов:


// Сервисы плагинов
class PluginA extends Context.Tag("PluginA")<PluginA, { process: (x: number) => Effect.Effect<number> }>() {}
class PluginB extends Context.Tag("PluginB")<PluginB, { process: (x: number) => Effect.Effect<number> }>() {}
class PluginC extends Context.Tag("PluginC")<PluginC, { process: (x: number) => Effect.Effect<number> }>() {}

// Задание: создайте функцию, которая:
// 1. Собирает все доступные плагины
// 2. Применяет их последовательно к входному значению
// 3. Возвращает финальный результат

const processWithPlugins = (
  input: number
): Effect.Effect<number, never, never> => {
  // Ваш код
  // Если PluginA доступен: input -> pluginA.process(input) -> result1
  // Если PluginB доступен: result1 -> pluginB.process(result1) -> result2
  // И так далее...
}
Упражнение

Lazy service initialization

Сложно

Реализуйте ленивую инициализацию сервиса:


class ExpensiveService extends Context.Tag("ExpensiveService")<
  ExpensiveService,
  { readonly compute: (input: string) => Effect.Effect<string> }
>() {}

// Задание: создайте обёртку, которая:
// 1. Инициализирует ExpensiveService только при первом использовании
// 2. Переиспользует инстанс при последующих вызовах
// 3. Requirements должен быть never (сервис создаётся лениво)

const createLazyService = (
  initializer: Effect.Effect<Context.Tag.Service<typeof ExpensiveService>>
): {
  readonly use: <A, E>(
    f: (service: Context.Tag.Service<typeof ExpensiveService>) => Effect.Effect<A, E, never>
  ) => Effect.Effect<A, E, never>
} => {
  // Ваш код
}

Заключение

В этой статье мы изучили:

  • 📖 yield Tag* — идиоматичный способ доступа в generators
  • 📖 Effect.service — явный доступ для pipe-стиля
  • 📖 Effect.serviceOption — опциональный доступ без изменения Requirements
  • 📖 Effect.serviceFunction — доступ + чистая трансформация
  • 📖 Effect.serviceFunctionEffect — доступ + эффективная трансформация
  • 📖 Effect.Tag — автоматический direct access к методам

💡 Ключевой takeaway: Effect предоставляет гибкий набор инструментов для доступа к сервисам. Выбирайте yield* для generator-стиля, serviceOption для опциональных зависимостей, и serviceFunction/serviceFunctionEffect для создания переиспользуемых accessor-ов.