Всё — выражение: let и значения

Понимаем фундаментальную идею: в OCaml почти любая конструкция — это выражение, у которого есть значение и тип.

Выражение — фрагмент кода, который вычисляется в значение определённого типа; в OCaml выражениями являются даже if и match.

В императивных языках есть чёткое деление на инструкции (что-то делают) и выражения (вычисляют значение). В OCaml эта граница почти стёрта: if, match, блок let ... in — всё это выражения, у каждого есть значение и тип. Это практическое удобство: вы можете подставить любое из них туда, где ожидается значение.

let связывает имя со значением

Ключевое слово let вводит связывание (binding) — даёт имени значение. Важно: это не «переменная»-ячейка, которую можно перезаписать. Это неизменяемое связывание: x навсегда означает то значение, с которым его связали.

let x = 10
let y = x * 2
let message = "Результат: " ^ string_of_int y

Здесь ^ — конкатенация строк, а string_of_int превращает число в строку. Каждое let на верхнем уровне модуля создаёт значение, видимое ниже по файлу.

Локальное let ... in

Внутри выражения связывание оформляется как let имя = значение in тело. Имя видно только в тело:

let area =
  let pi = 3.14159 in
  let r = 5.0 in
  pi *. r *. r

Здесь pi и r существуют только внутри вычисления area. Оператор *. — это умножение для чисел с плавающей точкой. Всё выражение let ... in ... само имеет значение — то, в которое вычисляется тело после in.

if — это тоже выражение

Поскольку if — выражение, обе ветки обязаны иметь один тип, и результат можно сразу присвоить:

let sign n =
  if n > 0 then "положительное"
  else if n < 0 then "отрицательное"
  else "ноль"

Здесь if сам по себе и есть значение функции sign. Если опустить ветку else у выражения, тип которого не unit, компилятор выдаст ошибку — иначе у выражения не было бы значения в одном из случаев.

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

Превращение всего в выражения — следствие происхождения OCaml из лямбда-исчисления, где программа — это вычисление выражения. Компилятор представляет код как дерево выражений, у каждого узла которого есть тип. Именно поэтому вывод типов работает так гладко: компилятор обходит дерево и собирает уравнения на типы. Неизменяемость связываний let упрощает рассуждения: вы знаете, что имя не «протухнет» дальше в коде.

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

  • Думать, что let x = 5 — присваивание. Это связывание; перезаписать x нельзя. Для изменяемости есть ref.
  • Забыть in в локальном связывании внутри функции — синтаксическая ошибка.
  • Считать = присваиванием. В OCaml = — сравнение значений; присваивания в обычном смысле тут нет.

Итоги

  • В OCaml почти всё — выражение со значением и типом: if, match, let ... in.
  • let имя = значение создаёт неизменяемое связывание, а не перезаписываемую переменную.
  • Локальное let ... in ограничивает видимость имени телом после in.
Проверьте себя
1. Что создаёт `let x = 5` в OCaml?
AИзменяемую переменную-ячейку
BНеизменяемое связывание имени со значением
CГлобальную константу C-уровня
DУказатель на 5
2. Почему у `if` в OCaml обе ветки должны иметь одинаковый тип?
AИз-за ленивости
BПотому что if — выражение и должно возвращать значение одного типа
CЭто требование линтера
DЧтобы ускорить компиляцию
3. Что ограничивает область видимости имени в `let r = 5.0 in тело`?
AВесь модуль
BТолько выражение после in
CСледующая функция
DФайл целиком