Ref<A>
Атомарные ссылки.
Теория
Проблема изменяемого состояния
В традиционном императивном программировании изменяемое состояние создаёт множество проблем:
// ❌ Проблемы с обычными переменными
let counter = 0
// Проблема 1: Race condition в конкурентном коде
async function increment() {
const current = counter // Чтение
await someAsyncWork()
counter = current + 1 // Запись — но counter мог измениться!
}
// Проблема 2: Нет гарантий атомарности
Promise.all([increment(), increment(), increment()])
// Ожидаем counter = 3, но можем получить 1, 2 или 3
// Проблема 3: Отсутствие типизации для эффектов
// Компилятор не знает, что функция изменяет состояние
Что такое Ref?
Ref<A> — это функциональная обёртка над изменяемым состоянием, которая:
- Атомарность — все операции чтения/записи атомарны
- Типобезопасность — изменение состояния явно выражено в типах
- Конкурентная безопасность — безопасно использовать из множества файберов
- Композируемость — операции над Ref — это Effect, их можно комбинировать
┌─────────────────────────────────────────────────────────┐
│ Ref<A> │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Внутреннее состояние │ │
│ │ value: A │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────────┐ │
│ │ get │ │ set │ │ update │ │
│ └──────┘ └──────┘ └──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Effect<A> Effect<void> Effect<void> │
│ │
│ ⚡ Все операции АТОМАРНЫ и ЭФФЕКТИВНЫЕ │
└─────────────────────────────────────────────────────────┘
Модель памяти и атомарность
Ref гарантирует compare-and-swap (CAS) семантику для операций обновления:
┌──────────────────────────────────────────────────────────┐
│ Compare-And-Swap (CAS) семантика │
├──────────────────────────────────────────────────────────┤
│ │
│ Fiber A Fiber B │
│ ─────── ─────── │
│ │ │ │
│ ├─ read(ref) → 10 │ │
│ │ ├─ read(ref) → 10 │
│ ├─ compute: 10 + 1 = 11 │ │
│ │ ├─ compute: 10 + 5 │
│ ├─ CAS(10 → 11) ✓ │ │
│ │ └── success! │ │
│ │ ├─ CAS(10 → 15) ✗ │
│ │ │ └── retry! │
│ │ ├─ read(ref) → 11 │
│ │ ├─ compute: 11 + 5 │
│ │ ├─ CAS(11 → 16) ✓ │
│ ▼ ▼ │
│ Final: ref = 16 │
│ │
└──────────────────────────────────────────────────────────┘
Ref vs обычные переменные
| Аспект | let / var | Ref<A> |
|---|---|---|
| Атомарность | ❌ Нет | ✅ Да |
| Type safety | ❌ Скрытые мутации | ✅ Явный Effect |
| Конкурентность | ❌ Race conditions | ✅ Безопасно |
| Композиция | ❌ Императивный код | ✅ Функциональный |
| Тестируемость | ❌ Сложно | ✅ Просто |
| Отладка | ❌ Сложно | ✅ Трассируемо |
Концепция ФП
State Monad и Ref
Ref можно рассматривать как реификацию State Monad в Effect:
// State Monad: S => (A, S)
// Ref делает состояние ВНЕШНИМ и РАЗДЕЛЯЕМЫМ
// Классический State Monad — состояние "проносится" через вычисление
type State<S, A> = (s: S) => readonly [A, S]
// Ref — состояние живёт ОТДЕЛЬНО, операции над ним — Effect
// Ref<A> + Effect = External Mutable State done right
Алгебраические свойства
Операции над Ref удовлетворяют важным алгебраическим законам:
// Закон 1: get после set возвращает установленное значение
// set(a) *> get === pure(a)
const law1 = Effect.gen(function* () {
const ref = yield* Ref.make(0)
yield* Ref.set(ref, 42)
const value = yield* Ref.get(ref)
// value === 42 ✓
})
// Закон 2: Два последовательных set — второй выигрывает
// set(a) *> set(b) === set(b)
const law2 = Effect.gen(function* () {
const ref = yield* Ref.make(0)
yield* Ref.set(ref, 1)
yield* Ref.set(ref, 2)
const value = yield* Ref.get(ref)
// value === 2 ✓
})
// Закон 3: update эквивалентен get + set (но атомарен!)
// update(f) === get.flatMap(a => set(f(a)))
// НО: update гарантирует атомарность, а get+set — нет!
Линейные типы и ownership
Хотя TypeScript не поддерживает линейные типы напрямую, Ref концептуально следует идее controlled aliasing:
// Ref создаётся один раз и передаётся явно
const program = Effect.gen(function* () {
// Создание — получаем "владение"
const ref = yield* Ref.make(0)
// Передача в функции — явная передача ссылки
yield* incrementBy(ref, 10)
yield* decrementBy(ref, 3)
// Ref не "утекает" за пределы Effect
return yield* Ref.get(ref)
})
const incrementBy = (ref: Ref.Ref<number>, n: number) =>
Ref.update(ref, (x) => x + n)
const decrementBy = (ref: Ref.Ref<number>, n: number) =>
Ref.update(ref, (x) => x - n)
API Reference
Создание Ref
Ref.make [STABLE]
Создаёт новый Ref с начальным значением.
declare const make: <A>(value: A) => Effect.Effect<Ref<A>>
// Ref с примитивным типом
const counterRef = Ref.make(0)
// ^? Effect<Ref<number>>
// Ref с объектом (иммутабельным!)
const userRef = Ref.make({ name: "Alice", age: 30 } as const)
// ^? Effect<Ref<{ readonly name: "Alice"; readonly age: 30 }>>
// Ref с ReadonlyArray
const itemsRef = Ref.make<ReadonlyArray<string>>([])
// ^? Effect<Ref<ReadonlyArray<string>>>
Чтение значения
Ref.get [STABLE]
Атомарно читает текущее значение.
declare const get: <A>(self: Ref<A>) => Effect.Effect<A>
const program = Effect.gen(function* () {
const ref = yield* Ref.make(42)
// Способ 1: Ref.get
const value1 = yield* Ref.get(ref)
// Способ 2: Pipeable API
const value2 = yield* ref.pipe(Ref.get)
// Способ 3: Метод на Ref (менее предпочтительно)
const value3 = yield* ref.get
return { value1, value2, value3 }
})
Запись значения
Ref.set [STABLE]
Атомарно устанавливает новое значение.
declare const set: <A>(value: A) => (self: Ref<A>) => Effect.Effect<void>
const program = Effect.gen(function* () {
const ref = yield* Ref.make("initial")
// Установка нового значения
yield* Ref.set(ref, "updated")
// Или через pipe
yield* ref.pipe(Ref.set("another"))
return yield* Ref.get(ref) // "another"
})
Атомарное обновление
Ref.update [STABLE]
Атомарно обновляет значение, применяя функцию.
declare const update: <A>(
f: (a: A) => A
) => (self: Ref<A>) => Effect.Effect<void>
const program = Effect.gen(function* () {
const ref = yield* Ref.make(10)
// Инкремент
yield* Ref.update(ref, (n) => n + 1)
// Умножение
yield* Ref.update(ref, (n) => n * 2)
// Сложная трансформация
yield* Ref.update(ref, (n) => Math.min(n, 100))
return yield* Ref.get(ref) // 22
})
Ref.updateAndGet [STABLE]
Атомарно обновляет и возвращает новое значение.
declare const updateAndGet: <A>(
f: (a: A) => A
) => (self: Ref<A>) => Effect.Effect<A>
const program = Effect.gen(function* () {
const ref = yield* Ref.make(5)
// Обновляем и сразу получаем новое значение
const doubled = yield* Ref.updateAndGet(ref, (n) => n * 2)
console.log(doubled) // 10
const incremented = yield* Ref.updateAndGet(ref, (n) => n + 1)
console.log(incremented) // 11
})
Ref.getAndUpdate [STABLE]
Атомарно возвращает текущее значение и обновляет.
declare const getAndUpdate: <A>(
f: (a: A) => A
) => (self: Ref<A>) => Effect.Effect<A>
const program = Effect.gen(function* () {
const ref = yield* Ref.make(5)
// Получаем старое значение, потом обновляем
const oldValue = yield* Ref.getAndUpdate(ref, (n) => n * 2)
console.log(oldValue) // 5 (старое)
const currentValue = yield* Ref.get(ref)
console.log(currentValue) // 10 (новое)
})
Ref.getAndSet [STABLE]
Атомарно возвращает текущее значение и устанавливает новое.
declare const getAndSet: <A>(
value: A
) => (self: Ref<A>) => Effect.Effect<A>
const program = Effect.gen(function* () {
const ref = yield* Ref.make("old")
// Swap-операция
const previous = yield* Ref.getAndSet(ref, "new")
console.log(previous) // "old"
console.log(yield* Ref.get(ref)) // "new"
})
Модификация с результатом
Ref.modify [STABLE]
Самая мощная операция — атомарно вычисляет результат и обновляет значение.
declare const modify: <A, B>(
f: (a: A) => readonly [B, A]
) => (self: Ref<A>) => Effect.Effect<B>
const program = Effect.gen(function* () {
const ref = yield* Ref.make(100)
// Снятие денег со счёта: возвращаем успех/неуспех и обновляем баланс
const withdraw = (amount: number) =>
Ref.modify(ref, (balance) => {
if (balance >= amount) {
return [true, balance - amount] as const // [результат, новое состояние]
}
return [false, balance] as const
})
const success1 = yield* withdraw(30)
console.log(success1) // true
const success2 = yield* withdraw(80)
console.log(success2) // false
console.log(yield* Ref.get(ref)) // 70
})
Ref.modifySome [STABLE]
Условная модификация — обновляет только если функция вернула Some.
declare const modifySome: <A, B>(
fallback: B,
f: (a: A) => Option.Option<readonly [B, A]>
) => (self: Ref<A>) => Effect.Effect<B>
const program = Effect.gen(function* () {
const ref = yield* Ref.make(10)
// Обновляем только если значение > 5
const result = yield* Ref.modifySome(
ref,
"skipped" as const, // fallback если условие не выполнено
(n) => n > 5
? Option.some(["updated", n * 2] as const)
: Option.none()
)
console.log(result) // "updated"
console.log(yield* Ref.get(ref)) // 20
})
Работа с коллекциями
Ref.updateSome [STABLE]
Условное обновление без возврата результата.
const program = Effect.gen(function* () {
const ref = yield* Ref.make<ReadonlyArray<number>>([1, 2, 3])
// Добавляем элемент только если массив не пуст
yield* Ref.updateSome(ref, (arr) =>
arr.length > 0
? Option.some([...arr, arr.length + 1])
: Option.none()
)
return yield* Ref.get(ref) // [1, 2, 3, 4]
})
Примеры
Базовый счётчик
// Определение интерфейса счётчика
interface Counter {
readonly inc: Effect.Effect<void>
readonly dec: Effect.Effect<void>
readonly get: Effect.Effect<number>
readonly reset: Effect.Effect<void>
}
// Создание счётчика
const makeCounter = (initial: number = 0): Effect.Effect<Counter> =>
Effect.gen(function* () {
const ref = yield* Ref.make(initial)
return {
inc: Ref.update(ref, (n) => n + 1),
dec: Ref.update(ref, (n) => n - 1),
get: Ref.get(ref),
reset: Ref.set(ref, initial)
} as const
})
// Использование
const program = Effect.gen(function* () {
const counter = yield* makeCounter(0)
yield* counter.inc
yield* counter.inc
yield* counter.inc
yield* counter.dec
const value = yield* counter.get
console.log(`Counter value: ${value}`) // Counter value: 2
yield* counter.reset
console.log(`After reset: ${yield* counter.get}`) // After reset: 0
})
Effect.runPromise(program)
Конкурентное обновление
const program = Effect.gen(function* () {
const ref = yield* Ref.make(0)
// Создаём 1000 конкурентных инкрементов
const increments = Array.from({ length: 1000 }, () =>
Ref.update(ref, (n) => n + 1)
)
// Запускаем все параллельно
yield* Effect.all(increments, { concurrency: "unbounded" })
const finalValue = yield* Ref.get(ref)
console.log(`Final value: ${finalValue}`) // Final value: 1000 (всегда!)
})
Effect.runPromise(program)
Ref как сервис
// Определяем Tag для сервиса состояния
class AppState extends Context.Tag("AppState")<
AppState,
Ref.Ref<{
readonly users: ReadonlyArray<string>
readonly isLoading: boolean
}>
>() {}
// Layer для создания начального состояния
const AppStateLive = Layer.effect(
AppState,
Ref.make({
users: [] as ReadonlyArray<string>,
isLoading: false
})
)
// Операции над состоянием
const addUser = (name: string) =>
Effect.gen(function* () {
const state = yield* AppState
yield* Ref.update(state, (s) => ({
...s,
users: [...s.users, name]
}))
})
const setLoading = (isLoading: boolean) =>
Effect.gen(function* () {
const state = yield* AppState
yield* Ref.update(state, (s) => ({ ...s, isLoading }))
})
const getUsers = Effect.gen(function* () {
const state = yield* AppState
const current = yield* Ref.get(state)
return current.users
})
// Программа
const program = Effect.gen(function* () {
yield* setLoading(true)
yield* addUser("Alice")
yield* addUser("Bob")
yield* setLoading(false)
const users = yield* getUsers
console.log("Users:", users)
})
// Запуск с предоставлением слоя
Effect.runPromise(program.pipe(Effect.provide(AppStateLive)))
// Output: Users: [ 'Alice', 'Bob' ]
Паттерн “Producer-Consumer” с Ref
interface Buffer<A> {
readonly push: (a: A) => Effect.Effect<void>
readonly pop: Effect.Effect<Option.Option<A>>
readonly size: Effect.Effect<number>
}
const makeBuffer = <A>(): Effect.Effect<Buffer<A>> =>
Effect.gen(function* () {
const ref = yield* Ref.make<ReadonlyArray<A>>([])
return {
push: (a: A) => Ref.update(ref, (items) => [...items, a]),
pop: Ref.modify(ref, (items) => {
if (items.length === 0) {
return [Option.none(), items] as const
}
const [head, ...tail] = items
return [Option.some(head!), tail] as const
}),
size: Effect.map(Ref.get(ref), (items) => items.length)
}
})
const program = Effect.gen(function* () {
const buffer = yield* makeBuffer<number>()
// Producer: добавляет элементы
const producer = Effect.gen(function* () {
for (let i = 1; i <= 5; i++) {
yield* buffer.push(i)
yield* Effect.log(`Produced: ${i}`)
yield* Effect.sleep("100 millis")
}
})
// Consumer: забирает элементы
const consumer = Effect.gen(function* () {
let consumed = 0
while (consumed < 5) {
const item = yield* buffer.pop
if (Option.isSome(item)) {
yield* Effect.log(`Consumed: ${item.value}`)
consumed++
} else {
yield* Effect.sleep("50 millis")
}
}
})
// Запускаем параллельно
yield* Effect.all([producer, consumer], { concurrency: 2 })
})
Effect.runPromise(program)
Immutable State Updates
// Типы для состояния приложения
interface Todo {
readonly id: string
readonly title: string
readonly completed: boolean
}
interface AppState {
readonly todos: ReadonlyArray<Todo>
readonly filter: "all" | "active" | "completed"
}
// Операции обновления (чистые функции)
const addTodo = (state: AppState, title: string): AppState => ({
...state,
todos: [
...state.todos,
{ id: crypto.randomUUID(), title, completed: false }
]
})
const toggleTodo = (state: AppState, id: string): AppState => ({
...state,
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})
const setFilter = (
state: AppState,
filter: AppState["filter"]
): AppState => ({
...state,
filter
})
// Селекторы (чистые функции)
const selectVisibleTodos = (state: AppState): ReadonlyArray<Todo> => {
switch (state.filter) {
case "active":
return state.todos.filter((t) => !t.completed)
case "completed":
return state.todos.filter((t) => t.completed)
default:
return state.todos
}
}
// Программа
const program = Effect.gen(function* () {
const stateRef = yield* Ref.make<AppState>({
todos: [],
filter: "all"
})
// Добавляем задачи
yield* Ref.update(stateRef, (s) => addTodo(s, "Learn Effect"))
yield* Ref.update(stateRef, (s) => addTodo(s, "Build app"))
yield* Ref.update(stateRef, (s) => addTodo(s, "Deploy"))
// Получаем состояние и первую задачу
const state1 = yield* Ref.get(stateRef)
const firstTodoId = state1.todos[0]?.id
if (firstTodoId) {
// Отмечаем первую задачу выполненной
yield* Ref.update(stateRef, (s) => toggleTodo(s, firstTodoId))
}
// Фильтруем по активным
yield* Ref.update(stateRef, (s) => setFilter(s, "active"))
// Получаем видимые задачи
const finalState = yield* Ref.get(stateRef)
const visibleTodos = selectVisibleTodos(finalState)
console.log("Visible todos:", visibleTodos)
})
Effect.runPromise(program)
Паттерны использования
Счётчик с метриками
interface Metrics {
readonly requestCount: number
readonly errorCount: number
readonly totalLatency: number
}
const makeMetrics = Effect.gen(function* () {
const ref = yield* Ref.make<Metrics>({
requestCount: 0,
errorCount: 0,
totalLatency: 0
})
return {
recordRequest: (latencyMs: number) =>
Ref.update(ref, (m) => ({
...m,
requestCount: m.requestCount + 1,
totalLatency: m.totalLatency + latencyMs
})),
recordError: Ref.update(ref, (m) => ({
...m,
errorCount: m.errorCount + 1
})),
getStats: Effect.gen(function* () {
const m = yield* Ref.get(ref)
return {
...m,
avgLatency: m.requestCount > 0
? m.totalLatency / m.requestCount
: 0
}
})
}
})
Кэш с ограничением размера
interface LRUCache<K, V> {
readonly get: (key: K) => Effect.Effect<Option.Option<V>>
readonly set: (key: K, value: V) => Effect.Effect<void>
readonly size: Effect.Effect<number>
}
const makeLRUCache = <K, V>(maxSize: number): Effect.Effect<LRUCache<K, V>> =>
Effect.gen(function* () {
// Храним Map с порядком вставки
const ref = yield* Ref.make<ReadonlyArray<readonly [K, V]>>([])
return {
get: (key: K) =>
Ref.modify(ref, (entries) => {
const index = entries.findIndex(([k]) => k === key)
if (index === -1) {
return [Option.none(), entries] as const
}
// Move to end (most recently used)
const entry = entries[index]!
const newEntries = [
...entries.slice(0, index),
...entries.slice(index + 1),
entry
]
return [Option.some(entry[1]), newEntries] as const
}),
set: (key: K, value: V) =>
Ref.update(ref, (entries) => {
// Удаляем существующий ключ если есть
const filtered = entries.filter(([k]) => k !== key)
// Добавляем в конец
const withNew = [...filtered, [key, value] as const]
// Обрезаем если превысили размер
return withNew.length > maxSize
? withNew.slice(-maxSize)
: withNew
}),
size: Effect.map(Ref.get(ref), (entries) => entries.length)
}
})
State Machine с Ref
// Состояния
type ConnectionState =
| { readonly _tag: "Disconnected" }
| { readonly _tag: "Connecting"; readonly attempt: number }
| { readonly _tag: "Connected"; readonly sessionId: string }
| { readonly _tag: "Disconnecting" }
// События
type ConnectionEvent =
| { readonly _tag: "Connect" }
| { readonly _tag: "ConnectionSuccess"; readonly sessionId: string }
| { readonly _tag: "ConnectionFailure" }
| { readonly _tag: "Disconnect" }
| { readonly _tag: "DisconnectComplete" }
// Transition function (чистая!)
const transition = (
state: ConnectionState,
event: ConnectionEvent
): ConnectionState => {
switch (state._tag) {
case "Disconnected":
return event._tag === "Connect"
? { _tag: "Connecting", attempt: 1 }
: state
case "Connecting":
switch (event._tag) {
case "ConnectionSuccess":
return { _tag: "Connected", sessionId: event.sessionId }
case "ConnectionFailure":
return state.attempt < 3
? { _tag: "Connecting", attempt: state.attempt + 1 }
: { _tag: "Disconnected" }
default:
return state
}
case "Connected":
return event._tag === "Disconnect"
? { _tag: "Disconnecting" }
: state
case "Disconnecting":
return event._tag === "DisconnectComplete"
? { _tag: "Disconnected" }
: state
}
}
// State machine с Ref
const makeConnectionMachine = Effect.gen(function* () {
const stateRef = yield* Ref.make<ConnectionState>({ _tag: "Disconnected" })
return {
send: (event: ConnectionEvent) =>
Ref.updateAndGet(stateRef, (s) => transition(s, event)),
getState: Ref.get(stateRef),
waitFor: (predicate: (s: ConnectionState) => boolean) =>
Effect.gen(function* () {
while (true) {
const state = yield* Ref.get(stateRef)
if (predicate(state)) return state
yield* Effect.sleep("10 millis")
}
})
}
})
Упражнения
Упражнение 1: Простой аккумулятор
Создайте функцию, которая суммирует все числа из массива, используя Ref.
import { Effect, Ref } from "effect"
const sumWithRef = (
numbers: ReadonlyArray<number>
): Effect.Effect<number> =>
// Ваш код здесь
Effect.gen(function* () {
// ???
})
// Тест
const test = Effect.gen(function* () {
const result = yield* sumWithRef([1, 2, 3, 4, 5])
console.assert(result === 15, `Expected 15, got ${result}`)
})import { Effect, Ref } from "effect"
const sumWithRef = (
numbers: ReadonlyArray<number>
): Effect.Effect<number> =>
Effect.gen(function* () {
const ref = yield* Ref.make(0)
for (const n of numbers) {
yield* Ref.update(ref, (sum) => sum + n)
}
return yield* Ref.get(ref)
})
// Альтернативное решение с Effect.forEach
const sumWithRefAlt = (
numbers: ReadonlyArray<number>
): Effect.Effect<number> =>
Effect.gen(function* () {
const ref = yield* Ref.make(0)
yield* Effect.forEach(
numbers,
(n) => Ref.update(ref, (sum) => sum + n),
{ discard: true }
)
return yield* Ref.get(ref)
})Упражнение 2: Toggle
Создайте Ref-based toggle с методами toggle, on, off, isOn.
import { Effect, Ref } from "effect"
interface Toggle {
readonly toggle: Effect.Effect<boolean> // возвращает новое состояние
readonly on: Effect.Effect<void>
readonly off: Effect.Effect<void>
readonly isOn: Effect.Effect<boolean>
}
const makeToggle = (initial: boolean = false): Effect.Effect<Toggle> =>
// Ваш код здесь
Effect.gen(function* () {
// ???
})import { Effect, Ref } from "effect"
interface Toggle {
readonly toggle: Effect.Effect<boolean>
readonly on: Effect.Effect<void>
readonly off: Effect.Effect<void>
readonly isOn: Effect.Effect<boolean>
}
const makeToggle = (initial: boolean = false): Effect.Effect<Toggle> =>
Effect.gen(function* () {
const ref = yield* Ref.make(initial)
return {
toggle: Ref.updateAndGet(ref, (v) => !v),
on: Ref.set(ref, true),
off: Ref.set(ref, false),
isOn: Ref.get(ref)
}
})Упражнение 3: Rate Counter
Создайте счётчик запросов в секунду с автоматическим сбросом.
import { Effect, Ref } from "effect"
interface RateCounter {
readonly increment: Effect.Effect<void>
readonly getRate: Effect.Effect<number> // запросов в секунду
}
const makeRateCounter = (): Effect.Effect<RateCounter> =>
// Ваш код здесь
Effect.gen(function* () {
// Подсказка: храните timestamp последнего сброса и count
// ???
})import { Effect, Ref } from "effect"
interface RateState {
readonly count: number
readonly windowStart: number // timestamp
}
interface RateCounter {
readonly increment: Effect.Effect<void>
readonly getRate: Effect.Effect<number>
}
const makeRateCounter = (): Effect.Effect<RateCounter> =>
Effect.gen(function* () {
const ref = yield* Ref.make<RateState>({
count: 0,
windowStart: Date.now()
})
const WINDOW_MS = 1000 // 1 секунда
return {
increment: Ref.update(ref, (state) => {
const now = Date.now()
// Если окно истекло, сбрасываем
if (now - state.windowStart >= WINDOW_MS) {
return { count: 1, windowStart: now }
}
return { ...state, count: state.count + 1 }
}),
getRate: Effect.gen(function* () {
const state = yield* Ref.get(ref)
const now = Date.now()
const elapsed = (now - state.windowStart) / 1000 // в секундах
// Если прошло меньше секунды, экстраполируем
return elapsed > 0 ? state.count / elapsed : state.count
})
}
})Упражнение 4: Undo/Redo Stack
Реализуйте структуру данных с поддержкой undo/redo.
import { Effect, Ref, Option } from "effect"
interface UndoableState<A> {
readonly get: Effect.Effect<A>
readonly set: (value: A) => Effect.Effect<void>
readonly undo: Effect.Effect<Option.Option<A>> // возвращает предыдущее состояние
readonly redo: Effect.Effect<Option.Option<A>> // возвращает следующее состояние
readonly canUndo: Effect.Effect<boolean>
readonly canRedo: Effect.Effect<boolean>
}
const makeUndoable = <A>(initial: A): Effect.Effect<UndoableState<A>> =>
// Ваш код здесь
Effect.gen(function* () {
// Подсказка: используйте два стека - past и future
// ???
})import { Effect, Ref, Option } from "effect"
interface History<A> {
readonly past: ReadonlyArray<A>
readonly present: A
readonly future: ReadonlyArray<A>
}
interface UndoableState<A> {
readonly get: Effect.Effect<A>
readonly set: (value: A) => Effect.Effect<void>
readonly undo: Effect.Effect<Option.Option<A>>
readonly redo: Effect.Effect<Option.Option<A>>
readonly canUndo: Effect.Effect<boolean>
readonly canRedo: Effect.Effect<boolean>
}
const makeUndoable = <A>(initial: A): Effect.Effect<UndoableState<A>> =>
Effect.gen(function* () {
const ref = yield* Ref.make<History<A>>({
past: [],
present: initial,
future: []
})
return {
get: Effect.map(Ref.get(ref), (h) => h.present),
set: (value: A) =>
Ref.update(ref, (h) => ({
past: [...h.past, h.present],
present: value,
future: [] // Сбрасываем future при новом изменении
})),
undo: Ref.modify(ref, (h) => {
if (h.past.length === 0) {
return [Option.none(), h] as const
}
const newPast = h.past.slice(0, -1)
const previous = h.past[h.past.length - 1]!
return [
Option.some(previous),
{
past: newPast,
present: previous,
future: [h.present, ...h.future]
}
] as const
}),
redo: Ref.modify(ref, (h) => {
if (h.future.length === 0) {
return [Option.none(), h] as const
}
const [next, ...restFuture] = h.future
return [
Option.some(next!),
{
past: [...h.past, h.present],
present: next!,
future: restFuture
}
] as const
}),
canUndo: Effect.map(Ref.get(ref), (h) => h.past.length > 0),
canRedo: Effect.map(Ref.get(ref), (h) => h.future.length > 0)
}
})Упражнение 5: CRDT Counter
Реализуйте CRDT G-Counter (Grow-only Counter) для распределённых систем.
import { Effect, Ref, HashMap } from "effect"
interface GCounter {
readonly nodeId: string
readonly increment: Effect.Effect<void>
readonly value: Effect.Effect<number>
readonly merge: (other: GCounterState) => Effect.Effect<void>
readonly getState: Effect.Effect<GCounterState>
}
// Состояние: Map от nodeId к локальному счётчику
type GCounterState = HashMap.HashMap<string, number>
const makeGCounter = (nodeId: string): Effect.Effect<GCounter> =>
// Ваш код здесь
Effect.gen(function* () {
// Подсказка: CRDT merge = поэлементный max
// ???
})import { Effect, Ref, HashMap, Option } from "effect"
interface GCounter {
readonly nodeId: string
readonly increment: Effect.Effect<void>
readonly value: Effect.Effect<number>
readonly merge: (other: GCounterState) => Effect.Effect<void>
readonly getState: Effect.Effect<GCounterState>
}
type GCounterState = HashMap.HashMap<string, number>
const makeGCounter = (nodeId: string): Effect.Effect<GCounter> =>
Effect.gen(function* () {
const ref = yield* Ref.make<GCounterState>(
HashMap.make([nodeId, 0])
)
return {
nodeId,
increment: Ref.update(ref, (state) => {
const current = HashMap.get(state, nodeId).pipe(
Option.getOrElse(() => 0)
)
return HashMap.set(state, nodeId, current + 1)
}),
value: Effect.map(Ref.get(ref), (state) => {
let sum = 0
for (const [, count] of state) {
sum += count
}
return sum
}),
merge: (other: GCounterState) =>
Ref.update(ref, (state) => {
let merged = state
for (const [key, otherCount] of other) {
const myCount = HashMap.get(state, key).pipe(
Option.getOrElse(() => 0)
)
// CRDT merge: берём максимум
merged = HashMap.set(merged, key, Math.max(myCount, otherCount))
}
return merged
}),
getState: Ref.get(ref)
}
})
// Демонстрация
const demo = Effect.gen(function* () {
const counter1 = yield* makeGCounter("node1")
const counter2 = yield* makeGCounter("node2")
// Независимые инкременты
yield* counter1.increment
yield* counter1.increment
yield* counter2.increment
// Получаем состояния
const state1 = yield* counter1.getState
const state2 = yield* counter2.getState
// Синхронизация (merge в обе стороны)
yield* counter1.merge(state2)
yield* counter2.merge(state1)
// Теперь оба видят одинаковое значение
const v1 = yield* counter1.value
const v2 = yield* counter2.value
console.log(`Counter 1: ${v1}`) // 3
console.log(`Counter 2: ${v2}`) // 3
})Упражнение 6: Concurrent Map с шардированием
Реализуйте конкурентный Map с шардированием для уменьшения contention.
import { Effect, Ref, HashMap, Option } from "effect"
interface ShardedMap<K, V> {
readonly get: (key: K) => Effect.Effect<Option.Option<V>>
readonly set: (key: K, value: V) => Effect.Effect<void>
readonly delete: (key: K) => Effect.Effect<boolean>
readonly size: Effect.Effect<number>
}
const makeShardedMap = <K, V>(
shardCount: number,
hashFn: (key: K) => number
): Effect.Effect<ShardedMap<K, V>> =>
// Ваш код здесь
Effect.gen(function* () {
// Подсказка: создайте массив из shardCount Ref'ов
// ???
})import { Effect, Ref, HashMap, Option, Array } from "effect"
interface ShardedMap<K, V> {
readonly get: (key: K) => Effect.Effect<Option.Option<V>>
readonly set: (key: K, value: V) => Effect.Effect<void>
readonly delete: (key: K) => Effect.Effect<boolean>
readonly size: Effect.Effect<number>
}
const makeShardedMap = <K, V>(
shardCount: number,
hashFn: (key: K) => number
): Effect.Effect<ShardedMap<K, V>> =>
Effect.gen(function* () {
// Создаём массив шардов (каждый шард — отдельный Ref)
const shards = yield* Effect.all(
Array.makeBy(shardCount, () =>
Ref.make<HashMap.HashMap<K, V>>(HashMap.empty())
)
)
// Функция для получения нужного шарда
const getShard = (key: K): Ref.Ref<HashMap.HashMap<K, V>> => {
const hash = hashFn(key)
const index = Math.abs(hash) % shardCount
return shards[index]!
}
return {
get: (key: K) => {
const shard = getShard(key)
return Effect.map(
Ref.get(shard),
(map) => HashMap.get(map, key)
)
},
set: (key: K, value: V) => {
const shard = getShard(key)
return Ref.update(shard, (map) => HashMap.set(map, key, value))
},
delete: (key: K) => {
const shard = getShard(key)
return Ref.modify(shard, (map) => {
const exists = HashMap.has(map, key)
return [exists, HashMap.remove(map, key)] as const
})
},
size: Effect.gen(function* () {
const sizes = yield* Effect.all(
shards.map((shard) =>
Effect.map(Ref.get(shard), HashMap.size)
)
)
return sizes.reduce((a, b) => a + b, 0)
})
}
})
// Пример использования
const demo = Effect.gen(function* () {
// Простая хэш-функция для строк
const stringHash = (s: string): number => {
let hash = 0
for (let i = 0; i < s.length; i++) {
hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0
}
return hash
}
const map = yield* makeShardedMap<string, number>(16, stringHash)
// Конкурентные записи
yield* Effect.all(
Array.makeBy(1000, (i) => map.set(`key-${i}`, i)),
{ concurrency: "unbounded" }
)
const size = yield* map.size
console.log(`Size: ${size}`) // 1000
const value = yield* map.get("key-42")
console.log(`key-42:`, value) // Some(42)
})Резюме
Ref<A> — это фундаментальный примитив для безопасного управления изменяемым состоянием в Effect:
| Ключевой аспект | Описание |
|---|---|
| Атомарность | Все операции атомарны, защита от race conditions |
| Type Safety | Мутации явно выражены в системе типов через Effect |
| Композиция | Операции над Ref композируемы с другими Effect |
| Иммутабельность | Храните иммутабельные данные внутри Ref |
| Сервисы | Используйте Ref как сервис через Context/Layer |
Когда использовать Ref
- ✅ Локальное состояние внутри Effect-программы
- ✅ Счётчики, метрики, аккумуляторы
- ✅ Кэши с простой логикой инвалидации
- ✅ State machines с синхронными переходами
- ✅ Разделяемое состояние между файберами
Когда использовать альтернативы
- ❌ Нужны эффективные обновления →
SynchronizedRef - ❌ Нужны транзакции над несколькими Ref →
STM - ❌ Нужна публикация изменений →
SubscriptionRef - ❌ Producer-Consumer очереди →
Queue