Effect Курс Ref<A>

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> — это функциональная обёртка над изменяемым состоянием, которая:

  1. Атомарность — все операции чтения/записи атомарны
  2. Типобезопасность — изменение состояния явно выражено в типах
  3. Конкурентная безопасность — безопасно использовать из множества файберов
  4. Композируемость — операции над 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 / varRef<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}`)
})
Упражнение

Упражнение 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* () {
    // ???
  })
Упражнение

Упражнение 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
    // ???
  })
Упражнение

Упражнение 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
    // ???
  })
Упражнение

Упражнение 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
    // ???
  })
Упражнение

Упражнение 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'ов
    // ???
  })

Резюме

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