Изменяемость: ref, mutable и массивы

Изучаем гибридную природу OCaml: как и когда выходить за рамки чистой функциональности с помощью изменяемого состояния.

ref — изменяемая ячейка, хранящая значение, которое можно перезаписать; вместе с mutable-полями и массивами это инструменты императивного стиля.

В отличие от Haskell, где мутация загнана в монаду, OCaml относится к изменяемости спокойно: если она делает код проще или быстрее — пожалуйста. Это и есть прагматизм языка. Но важно применять эти инструменты осознанно, не превращая функциональный код в императивный без нужды.

Ссылки ref

let counter = ref 0      (* int ref *)
let () = counter := !counter + 1   (* записать *)
let v = !counter                   (* прочитать: 1 *)

ref 0 создаёт ячейку со значением 0, !counter читает её содержимое, counter := ... записывает новое. По сути ref — это запись с одним изменяемым полем contents. Сама переменная counter неизменна, меняется только содержимое.

Изменяемые поля mutable

type account = { owner : string; mutable balance : int }

let acc = { owner = "Аня"; balance = 100 }
let () = acc.balance <- acc.balance + 50   (* теперь 150 *)

Оператор <- присваивает новое значение изменяемому полю. Поле owner остаётся неизменным — мутировать можно только помеченные mutable.

Массивы

let a = [| 10; 20; 30 |]    (* int array *)
let x = a.(0)               (* чтение: 10 *)
let () = a.(1) <- 99        (* запись: теперь [|10; 99; 30|] *)
let n = Array.length a      (* 3 *)

Литерал массива — в [| ... |], доступ — a.(i) (круглые скобки, в отличие от .[i] у строк). Массивы хороши там, где нужен частый произвольный доступ — то, в чём списки слабы.

Когда использовать изменяемость

ЗадачаИнструмент
счётчик, накопительref
изменяемое поле в структуреmutable
буфер с доступом по индексуarray
кеш, мемоизацияHashtbl

Как работает под капотом

ref — не магия, а обычная запись: type 'a ref = { mutable contents : 'a }, а !r и r := v — сахар над r.contents и r.contents <- v. Изменяемые поля и массивы хранятся в куче, и сборщик мусора отслеживает «обратные ссылки» через write barrier — поэтому мутация чуть дороже чтения, но всё равно быстра. Прагматичный совет: держите мутацию локальной. Часто изящнее реализовать функцию императивно внутри, но снаружи дать чистый интерфейс.

Частые ошибки

  • Забыть ! при чтении ref. counter — это ячейка, !counter — её значение.
  • Путать := и <-. := — для ref, <- — для mutable-полей и массивов.
  • Делать всё на ref по привычке. Сначала ищите чистое решение.

Итоги

  • ref — изменяемая ячейка: ref v создаёт, !r читает, r := v пишет.
  • mutable-поля меняются оператором <-; массивы дают доступ по индексу за O(1).
  • Прагматичный стиль: мутация локально внутри, чистый интерфейс снаружи.
Проверьте себя
1. Как прочитать значение из ref-ячейки `r`?
Ar
B!r
C*r
Dr.value
2. Каким оператором меняют значение mutable-поля записи?
A:=
B<-
C=
D->
3. Какое практичное правило работы с изменяемостью даёт OCaml?
AНикогда не использовать мутацию
BДержать мутацию локальной внутри, давая чистый интерфейс снаружи
CИспользовать ref для всего
DМутация запрещена компилятором