Доступ к сервисам
Способы доступа к сервисам в Effect.
Теория
Способы доступа к сервисам
Effect предоставляет несколько механизмов для получения сервисов из Context. Каждый имеет своё предназначение:
| Метод | Возвращает | Когда использовать |
|---|---|---|
yield* Tag | Сервис напрямую | Стандартный способ в generators |
Effect.service | Effect с сервисом | Альтернатива для pipe-стиля |
Effect.serviceOption | Effect с Option | Опциональные зависимости |
Effect.serviceFunction | Effect с результатом функции | Однострочный доступ + трансформация |
Effect.serviceFunctionEffect | Effect с вложенным 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
| Reader | Effect | Описание |
|---|---|---|
ask | yield* 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> = // Ваш кодconst program: Effect.Effect<string, never, Random> = Effect.gen(function* () {
const random = yield* Random
const number = yield* random.nextInt(101) // 0-100
const boolean = yield* random.nextBoolean()
return `Number: ${number}, Boolean: ${boolean}`
})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> = // ...const getTheme = Effect.serviceFunction(Settings, (s) => () => s.theme)
const getLanguage = Effect.serviceFunction(Settings, (s) => () => s.language)
const areNotificationsEnabled = Effect.serviceFunction(Settings, (s) => () => s.notifications)
const getFontSize = Effect.serviceFunction(Settings, (s) => () => s.fontSize)
// Использование
const program = Effect.gen(function* () {
const theme = yield* getTheme()
const lang = yield* getLanguage()
console.log(`${lang}: Theme is ${theme}`)
})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> = // ...const maybeTrack = (event: string): Effect.Effect<boolean, never, never> =>
Effect.gen(function* () {
const maybeAnalytics = yield* Effect.serviceOption(Analytics)
if (Option.isSome(maybeAnalytics)) {
yield* maybeAnalytics.value.track(event)
return true
}
return false
})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> => // ...// Accessor functions
export const getProductStock = Effect.serviceFunctionEffect(Inventory, (i) => i.getStock)
export const reserveProduct = Effect.serviceFunctionEffect(Inventory, (i) => i.reserve)
export const releaseProduct = Effect.serviceFunctionEffect(Inventory, (i) => i.release)
// Хелпер
export const isInStock = (productId: string, minQuantity: number = 1):
Effect.Effect<boolean, ProductNotFound, Inventory> =>
getProductStock(productId).pipe(
Effect.map((stock) => stock >= minQuantity)
)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> => // ...const encryptIfAvailable = (data: string): Effect.Effect<string, never, never> =>
Effect.gen(function* () {
const maybeEncryption = yield* Effect.serviceOption(EncryptionService)
if (Option.isSome(maybeEncryption)) {
return yield* maybeEncryption.value.encrypt(data)
}
return data
})
const decryptIfAvailable = (data: string): Effect.Effect<string, never, never> =>
Effect.gen(function* () {
const maybeEncryption = yield* Effect.serviceOption(EncryptionService)
if (Option.isSome(maybeEncryption)) {
return yield* maybeEncryption.value.decrypt(data)
}
return data
})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> => // ...interface UserProfile {
readonly user: User
readonly orders: ReadonlyArray<Order>
readonly balance: number
}
const getUserProfile = (userId: string):
Effect.Effect<UserProfile | null, never, UserService | OrderService | PaymentService> =>
Effect.gen(function* () {
const userService = yield* UserService
const orderService = yield* OrderService
const paymentService = yield* PaymentService
const user = yield* userService.findById(userId)
if (!user) return null
const [orders, balance] = yield* Effect.all([
orderService.findByUserId(userId),
paymentService.getBalance(userId)
])
return { user, orders, balance }
})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")const createAccessor = <T extends Context.Tag<any, any>, K extends keyof Context.Tag.Service<T>>(
tag: T,
method: K
) => {
return (...args: any[]) =>
Effect.gen(function* () {
const service = yield* tag
const fn = service[method] as (...args: any[]) => Effect.Effect<any, any, any>
return yield* fn(...args)
}) as any
}
// Использование
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")
const program = Effect.gen(function* () {
const results = yield* query("SELECT * FROM users")
const count = yield* execute("UPDATE users SET active = true")
return { results, count }
})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
// И так далее...
}const processWithPlugins = (
input: number
): Effect.Effect<number, never, never> =>
Effect.gen(function* () {
const maybeA = yield* Effect.serviceOption(PluginA)
const maybeB = yield* Effect.serviceOption(PluginB)
const maybeC = yield* Effect.serviceOption(PluginC)
let result = input
if (Option.isSome(maybeA)) {
result = yield* maybeA.value.process(result)
}
if (Option.isSome(maybeB)) {
result = yield* maybeB.value.process(result)
}
if (Option.isSome(maybeC)) {
result = yield* maybeC.value.process(result)
}
return result
})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>
} => {
// Ваш код
}const createLazyService = (
initializer: Effect.Effect<Context.Tag.Service<typeof ExpensiveService>>
) => {
let cachedService: Context.Tag.Service<typeof ExpensiveService> | null = null
return {
use: <A, E>(
f: (service: Context.Tag.Service<typeof ExpensiveService>) => Effect.Effect<A, E, never>
): Effect.Effect<A, E, never> =>
Effect.gen(function* () {
if (!cachedService) {
cachedService = yield* initializer
}
return yield* f(cachedService)
})
}
}
// Использование
const lazyService = createLazyService(
Effect.sync(() => ({
compute: (input: string) => Effect.succeed(`Computed: ${input}`)
}))
)
const program = lazyService.use((service) =>
service.compute("test")
)Заключение
В этой статье мы изучили:
- 📖 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-ов.