Effect Курс MutableRef

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) => booleanCAS операция
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
}
Упражнение

Упражнение 2: State Machine с MutableRef

Средне

Реализуйте простой конечный автомат для парсинга CSV с использованием MutableRef:

import { MutableRef } from "effect"

const parseCSVLine = (line: string): ReadonlyArray<string> => {
  // Ваш код
  // Парсинг с учётом кавычек: 'a,"b,c",d' → ["a", "b,c", "d"]
}
Упражнение

Упражнение 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> => {
  // Ваш код
}