Значения и неизменяемость: let и иммутабельность по умолчанию

Главный сдвиг мышления: в F# значения неизменяемы по умолчанию, а не переменны.

Неизменяемость означает, что однажды связанное значение нельзя поменять; let x = 5 привязывает имя к значению, а не объявляет ячейку для перезаписи.

let привязывает, а не присваивает

В императивных языках x = 5 — это присваивание ячейке, которую потом можно перезаписать. В F# let x = 5 — это связывание (binding): имя x навсегда означает 5 в своей области видимости.

let x = 5
// x <- 6   // ОШИБКА: значение неизменяемо

Попытка изменить x не скомпилируется. Это не ограничение, а гарантия: вы точно знаете, что значение под именем не «уплывёт» из-под ног в другой части программы.

Зачем неизменяемость

Изменяемое состояние — источник большинства коварных багов: что-то меняет значение, и поведение программы зависит от порядка и времени изменений. Неизменяемые данные:

  • Предсказуемы — значение не меняется, рассуждать о коде проще.
  • Безопасны для потоков — нечего «защищать» от гонок, можно свободно делить между потоками.
  • Удобны для отладки — состояние не «портится» в неожиданный момент.

Когда нужна мутация: mutable

F# прагматичен: если мутация действительно нужна (счётчик в цикле, буфер), явно объявите mutable и присваивайте оператором <-.

let mutable counter = 0
counter <- counter + 1
counter <- counter + 1
printfn "%d" counter

Вывод:

2

Ключевое слово mutable делает мутацию видимой в коде: читатель сразу понимает, что здесь состояние меняется. По умолчанию же таких сюрпризов нет.

Затенение (shadowing)

Иногда хочется «переиспользовать» имя, не нарушая неизменяемость. F# позволяет затенить прежнюю привязку новой с тем же именем.

let value = 10
let value = value + 5   // новая привязка, прежняя value = 10 затенена
printfn "%d" value

Вывод:

15

Это не мутация: создаётся новое неизменяемое значение, а старое перестаёт быть видимым. Тип при этом может даже поменяться.

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

Неизменяемое связывание компилируется в обычное поле/локальную переменную IL, которая просто не перезаписывается — никаких накладных расходов в рантайме. mutable-значения становятся изменяемыми ячейками. Затенение — это две независимые привязки на уровне компилятора, во второй области видимости имя ссылается на новое значение; старое существует, но недоступно по имени.

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

  • Пытаться писать x <- ... для обычного let — нужно mutable.
  • Путать затенение с мутацией: затенение создаёт новое значение, не меняя старое.
  • Злоупотреблять mutable по привычке из C# — обычно есть функциональная альтернатива (рекурсия, fold).

Итоги

  • let создаёт неизменяемое связывание имени со значением.
  • Неизменяемость по умолчанию делает код предсказуемым и потокобезопасным.
  • Для мутации нужны mutable и оператор <- — она становится явной.
  • Затенение переиспользует имя через новую привязку, не меняя старое значение.
Проверьте себя
1. Что делает let x = 5 в F#?
AОбъявляет изменяемую переменную
BСоздаёт неизменяемое связывание имени со значением
CОбъявляет константу времени компиляции в C-стиле
DСоздаёт глобальную переменную с автомутацией
2. Как сделать значение изменяемым?
AПоставить var
BОбъявить mutable и присваивать оператором <-
CИспользовать const
DДобавить @mutable атрибут
3. Что такое затенение (shadowing)?
AМутация прежнего значения
BНовая привязка того же имени, не меняющая старое значение
CУдаление переменной
DСкрытие переменной от компилятора