Изменяемость: 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).- Прагматичный стиль: мутация локально внутри, чистый интерфейс снаружи.