MutableRef
Когда нужна мутабельность.
Теория
Зачем нужна мутабельность
В строго функциональном программировании мутабельность считается источником багов. Однако в определённых ситуациях контролируемая мутабельность оправдана:
Когда мутабельность допустима:
┌─────────────────────────────────────────────┐
│ │
│ 1. Локальная мутабельность │
│ • Переменная не покидает функцию │
│ • Результат иммутабельный │
│ │
│ 2. Производительность │
│ • Hot path с частыми обновлениями │
│ • Аккумулятор в цикле │
│ │
│ 3. Interop │
│ • Взаимодействие с мутабельными API │
│ • Callback-based интерфейсы │
│ │
│ 4. Метрики и счётчики │
│ • Однопоточный сбор статистики │
│ • Performance counters │
│ │
└─────────────────────────────────────────────┘
Что такое MutableRef
MutableRef<A> — это простой мутабельный контейнер для значения типа A:
MutableRef<number>
┌─────────────────────────┐
│ │
│ value: 42 │
│ │
│ get() → 42 │
│ set(7) → void │
│ get() → 7 │
│ │
│ ⚠️ НЕ thread-safe │
│ ⚠️ НЕ эффектовый │
│ ✅ Синхронный │
│ ✅ Нулевой overhead │
│ │
└─────────────────────────┘
MutableRef vs Ref
Важно не путать MutableRef с Ref из модуля конкурентности:
┌──────────────────────┬─────────────────┬─────────────────────┐
│ Характеристика │ MutableRef<A> │ Ref<A> │
├──────────────────────┼─────────────────┼─────────────────────┤
│ Модуль │ effect │ effect │
│ Мутабельность │ In-place │ Атомарная │
│ Thread-safety │ ❌ Нет │ ✅ Да │
│ Тип операций │ Синхронные │ Effect (эффектовые) │
│ get │ A │ Effect<A> │
│ set │ void │ Effect<void> │
│ update │ A │ Effect<A> │
│ Overhead │ ~0 │ Минимальный │
│ Concurrency │ ❌ │ ✅ Fiber-safe │
│ Использование │ Локальные │ Shared state │
│ │ оптимизации │ между Fiber-ами │
└──────────────────────┴─────────────────┴─────────────────────┘
Правило выбора:
Нужен shared state между Fiber-ами?
├── Да → Ref<A> (атомарный, fiber-safe)
└── Нет
├── Нужен в Effect контексте?
│ ├── Да → Ref<A>
│ └── Нет → MutableRef<A>
└── Локальная оптимизация?
└── Да → MutableRef<A> или let
Создание MutableRef
make — создание с начальным значением
// Создание MutableRef с числовым значением
const counter = MutableRef.make(0)
// С строковым значением
const name = MutableRef.make("initial")
// С объектом
const config = MutableRef.make({
host: "localhost",
port: 3000
} as const)
API Reference
| Функция | Сигнатура | Описание |
|---|---|---|
make | (value: A) => MutableRef<A> | Создать MutableRef |
get | (ref: MutableRef<A>) => A | Получить текущее значение |
set | (ref: MutableRef<A>, value: A) => MutableRef<A> | Установить значение |
update | (ref: MutableRef<A>, f: A => A) => MutableRef<A> | Обновить через функцию |
getAndSet | (ref: MutableRef<A>, value: A) => A | Получить старое, установить новое |
getAndUpdate | (ref: MutableRef<A>, f: A => A) => A | Получить старое, обновить |
updateAndGet | (ref: MutableRef<A>, f: A => A) => A | Обновить и получить новое |
setAndGet | (ref: MutableRef<A>, value: A) => A | Установить и получить |
compareAndSet | (ref: MutableRef<A>, old: A, next: A) => boolean | CAS операция |
toggle | (ref: MutableRef<boolean>) => MutableRef<boolean> | Переключить boolean |
increment | (ref: MutableRef<number>) => MutableRef<number> | +1 для числовых |
decrement | (ref: MutableRef<number>) => MutableRef<number> | -1 для числовых |
Основные операции
get / set — чтение и запись
const ref = MutableRef.make(42)
// Чтение — возвращает значение напрямую (не Effect!)
const value = MutableRef.get(ref)
console.log(value) // 42
// Запись — мутирует на месте, возвращает ref для chaining
MutableRef.set(ref, 100)
console.log(MutableRef.get(ref)) // 100
update — обновление через функцию
const counter = MutableRef.make(0)
// Обновить значение через функцию
MutableRef.update(counter, (n) => n + 1)
MutableRef.update(counter, (n) => n + 1)
MutableRef.update(counter, (n) => n + 1)
console.log(MutableRef.get(counter)) // 3
getAndSet / getAndUpdate — атомарное чтение + запись
const ref = MutableRef.make(10)
// Получить старое значение и установить новое
const old = MutableRef.getAndSet(ref, 20)
console.log(old) // 10
console.log(MutableRef.get(ref)) // 20
// Получить старое значение и обновить
const ref2 = MutableRef.make(5)
const prev = MutableRef.getAndUpdate(ref2, (n) => n * 2)
console.log(prev) // 5
console.log(MutableRef.get(ref2)) // 10
updateAndGet / setAndGet — обновление + чтение нового
const ref = MutableRef.make(5)
// Обновить и получить НОВОЕ значение
const newValue = MutableRef.updateAndGet(ref, (n) => n * 3)
console.log(newValue) // 15
toggle — переключение boolean
const flag = MutableRef.make(false)
MutableRef.toggle(flag)
console.log(MutableRef.get(flag)) // true
MutableRef.toggle(flag)
console.log(MutableRef.get(flag)) // false
increment / decrement — числовые операции
const counter = MutableRef.make(0)
MutableRef.increment(counter)
MutableRef.increment(counter)
MutableRef.increment(counter)
console.log(MutableRef.get(counter)) // 3
MutableRef.decrement(counter)
console.log(MutableRef.get(counter)) // 2
compareAndSet — условная замена
const ref = MutableRef.make(42)
// Заменить только если текущее значение === expected
const wasSwapped = MutableRef.compareAndSet(ref, 42, 100)
console.log(wasSwapped) // true
console.log(MutableRef.get(ref)) // 100
const wasSwapped2 = MutableRef.compareAndSet(ref, 42, 200)
console.log(wasSwapped2) // false — текущее 100, а не 42
console.log(MutableRef.get(ref)) // 100 — не изменилось
Когда использовать MutableRef
Хорошие use cases
1. Счётчик в цикле обработки
const processItems = (items: ReadonlyArray<string>): {
readonly processed: number
readonly errors: number
} => {
const processed = MutableRef.make(0)
const errors = MutableRef.make(0)
for (const item of items) {
if (item.length > 0) {
MutableRef.increment(processed)
} else {
MutableRef.increment(errors)
}
}
return {
processed: MutableRef.get(processed),
errors: MutableRef.get(errors)
}
}
// Результат иммутабельный, мутабельность локальна
2. Аккумулятор для построения результата
const buildReport = (data: ReadonlyArray<number>): string => {
const result = MutableRef.make("")
MutableRef.update(result, (s) => s + "Report:\n")
for (const value of data) {
MutableRef.update(result, (s) => s + ` Value: ${value}\n`)
}
MutableRef.update(result, (s) => s + "End of report")
return MutableRef.get(result)
}
3. Cache hit/miss counter
interface CacheStats {
readonly hits: number
readonly misses: number
readonly hitRate: number
}
const createCacheTracker = () => {
const hits = MutableRef.make(0)
const misses = MutableRef.make(0)
return {
recordHit: () => MutableRef.increment(hits),
recordMiss: () => MutableRef.increment(misses),
getStats: (): CacheStats => {
const h = MutableRef.get(hits)
const m = MutableRef.get(misses)
const total = h + m
return {
hits: h,
misses: m,
hitRate: total === 0 ? 0 : h / total
}
}
}
}
4. Toggle состояние
const createCircuitBreaker = () => {
const isOpen = MutableRef.make(false)
const failureCount = MutableRef.make(0)
const threshold = 5
return {
recordFailure: () => {
MutableRef.increment(failureCount)
if (MutableRef.get(failureCount) >= threshold) {
MutableRef.set(isOpen, true)
}
},
reset: () => {
MutableRef.set(failureCount, 0)
MutableRef.set(isOpen, false)
},
isAvailable: () => !MutableRef.get(isOpen)
}
}
Антипаттерны
Не используйте MutableRef для shared state
// ❌ НЕПРАВИЛЬНО — shared state между Fiber-ами
const badCounter = MutableRef.make(0)
const badProgram = Effect.all(
Array.from({ length: 100 }, () =>
Effect.sync(() => MutableRef.increment(badCounter))
),
{ concurrency: "unbounded" }
)
// Race condition! Результат непредсказуем
// ✅ ПРАВИЛЬНО — используйте Ref для конкурентного доступа
const goodProgram = Effect.gen(function* () {
const counter = yield* Ref.make(0)
yield* Effect.all(
Array.from({ length: 100 }, () =>
Ref.update(counter, (n) => n + 1)
),
{ concurrency: "unbounded" }
)
return yield* Ref.get(counter)
})
// Всегда 100!
Не передавайте MutableRef как аргумент функции
// ❌ НЕПРАВИЛЬНО — MutableRef покидает локальный scope
const processData = (counter: MutableRef.MutableRef<number>) => {
MutableRef.increment(counter)
// Кто ещё мутирует этот counter? Непредсказуемо!
}
// ✅ ПРАВИЛЬНО — мутабельность внутри, результат иммутабельный
const processData2 = (items: ReadonlyArray<string>): number => {
const counter = MutableRef.make(0)
for (const item of items) {
if (item.length > 0) MutableRef.increment(counter)
}
return MutableRef.get(counter) // возвращаем иммутабельное значение
}
Не используйте вместо let
// ❌ Переусложнение — просто используйте let
const ref = MutableRef.make(0)
MutableRef.set(ref, 42)
const result = MutableRef.get(ref)
// ✅ Проще
let value = 0
value = 42
const result2 = value
MutableRef оправдан когда: есть несколько операций update/getAndSet или когда семантика “контейнера со значением” делает код яснее.
Паттерны использования
Builder pattern
interface QueryConfig {
readonly table: string
readonly fields: ReadonlyArray<string>
readonly where: string | null
readonly limit: number | null
readonly orderBy: string | null
}
const queryBuilder = (table: string) => {
const config = MutableRef.make<QueryConfig>({
table,
fields: [],
where: null,
limit: null,
orderBy: null
})
return {
select: (...fields: ReadonlyArray<string>) => {
MutableRef.update(config, (c) => ({ ...c, fields }))
return builder
},
where: (condition: string) => {
MutableRef.update(config, (c) => ({ ...c, where: condition }))
return builder
},
limit: (n: number) => {
MutableRef.update(config, (c) => ({ ...c, limit: n }))
return builder
},
orderBy: (field: string) => {
MutableRef.update(config, (c) => ({ ...c, orderBy: field }))
return builder
},
build: (): QueryConfig => MutableRef.get(config)
}
// Self-reference для chaining
const builder = {
select: (...fields: ReadonlyArray<string>) => {
MutableRef.update(config, (c) => ({ ...c, fields }))
return builder
},
where: (condition: string) => {
MutableRef.update(config, (c) => ({ ...c, where: condition }))
return builder
},
limit: (n: number) => {
MutableRef.update(config, (c) => ({ ...c, limit: n }))
return builder
},
orderBy: (field: string) => {
MutableRef.update(config, (c) => ({ ...c, orderBy: field }))
return builder
},
build: (): QueryConfig => MutableRef.get(config)
}
return builder
}
Memoization
const memoize = <A, B>(fn: (a: A) => B) => {
const cache = MutableRef.make(HashMap.empty<A, B>())
return (a: A): B => {
const cached = HashMap.get(MutableRef.get(cache), a)
if (Option.isSome(cached)) return cached.value
const result = fn(a)
MutableRef.update(cache, (c) => HashMap.set(c, a, result))
return result
}
}
const fibonacci = memoize((n: number): number =>
n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2)
)
Упражнения
Упражнение 1: Running Statistics
Реализуйте функцию, вычисляющую running average и max:
import { MutableRef } from "effect"
const computeStats = (values: ReadonlyArray<number>): {
readonly average: number
readonly max: number
readonly count: number
} => {
// Ваш код с MutableRef
}import { MutableRef } from "effect"
const computeStats = (values: ReadonlyArray<number>): {
readonly average: number
readonly max: number
readonly count: number
} => {
const sum = MutableRef.make(0)
const max = MutableRef.make(-Infinity)
const count = MutableRef.make(0)
for (const value of values) {
MutableRef.update(sum, (s) => s + value)
MutableRef.update(max, (m) => Math.max(m, value))
MutableRef.increment(count)
}
const c = MutableRef.get(count)
return {
average: c === 0 ? 0 : MutableRef.get(sum) / c,
max: c === 0 ? 0 : MutableRef.get(max),
count: c
}
}Упражнение 2: State Machine с MutableRef
Реализуйте простой конечный автомат для парсинга CSV с использованием MutableRef:
import { MutableRef } from "effect"
const parseCSVLine = (line: string): ReadonlyArray<string> => {
// Ваш код
// Парсинг с учётом кавычек: 'a,"b,c",d' → ["a", "b,c", "d"]
}import { MutableRef } from "effect"
const parseCSVLine = (line: string): ReadonlyArray<string> => {
const fields: Array<string> = []
const current = MutableRef.make("")
const inQuotes = MutableRef.make(false)
for (const char of line) {
if (char === '"') {
MutableRef.toggle(inQuotes)
} else if (char === ',' && !MutableRef.get(inQuotes)) {
fields.push(MutableRef.getAndSet(current, ""))
} else {
MutableRef.update(current, (s) => s + char)
}
}
fields.push(MutableRef.get(current))
return fields
}Упражнение 3: Ring Buffer
Реализуйте кольцевой буфер фиксированного размера на MutableRef:
import { MutableRef, Option } from "effect"
interface RingBuffer<A> {
readonly push: (value: A) => void
readonly pop: () => Option.Option<A>
readonly peek: () => Option.Option<A>
readonly size: () => number
readonly toArray: () => ReadonlyArray<A>
}
const createRingBuffer = <A>(capacity: number): RingBuffer<A> => {
// Ваш код
}import { MutableRef, Option } from "effect"
interface RingBuffer<A> {
readonly push: (value: A) => void
readonly pop: () => Option.Option<A>
readonly peek: () => Option.Option<A>
readonly size: () => number
readonly toArray: () => ReadonlyArray<A>
}
const createRingBuffer = <A>(capacity: number): RingBuffer<A> => {
const buffer = MutableRef.make<Array<A | undefined>>(
new Array(capacity).fill(undefined)
)
const head = MutableRef.make(0)
const tail = MutableRef.make(0)
const count = MutableRef.make(0)
return {
push: (value: A) => {
const buf = MutableRef.get(buffer)
const t = MutableRef.get(tail)
buf[t] = value
MutableRef.set(tail, (t + 1) % capacity)
if (MutableRef.get(count) === capacity) {
// Перезапись — двигаем head
MutableRef.set(head, (MutableRef.get(head) + 1) % capacity)
} else {
MutableRef.increment(count)
}
},
pop: () => {
if (MutableRef.get(count) === 0) return Option.none()
const buf = MutableRef.get(buffer)
const h = MutableRef.get(head)
const value = buf[h] as A
buf[h] = undefined
MutableRef.set(head, (h + 1) % capacity)
MutableRef.decrement(count)
return Option.some(value)
},
peek: () => {
if (MutableRef.get(count) === 0) return Option.none()
return Option.some(MutableRef.get(buffer)[MutableRef.get(head)] as A)
},
size: () => MutableRef.get(count),
toArray: () => {
const result: Array<A> = []
const buf = MutableRef.get(buffer)
const h = MutableRef.get(head)
const c = MutableRef.get(count)
for (let i = 0; i < c; i++) {
result.push(buf[(h + i) % capacity] as A)
}
return result
}
}
}