Алгебраические типы: моделируем домен

Знакомимся с самым выразительным инструментом OCaml для моделирования данных — алгебраическими (вариантными) типами.

Вариантный тип (sum type) описывает значение, которое может быть одной из нескольких альтернатив, каждая со своим набором данных.

Записи отвечают на вопрос «И то, и то» (имя и возраст), а вариантные типы — на вопрос «Или то, или то» (платёж или наличными, или картой). Вместе они дают алгебраические типы данных — способ описать домен так точно, что многие ошибки становятся невыразимыми.

Простые варианты

type color = Red | Green | Blue

let to_hex = function
  | Red   -> "#FF0000"
  | Green -> "#00FF00"
  | Blue  -> "#0000FF"

Имена вариантов (конструкторы) пишутся с заглавной буквы. Здесь function — сокращение для fun x -> match x with ....

Варианты с данными

type shape =
  | Circle of float                 (* радиус *)
  | Rectangle of float * float      (* ширина, высота *)
  | Triangle of float * float       (* основание, высота *)

let area = function
  | Circle r -> 3.14159 *. r *. r
  | Rectangle (w, h) -> w *. h
  | Triangle (b, h) -> 0.5 *. b *. h

let s = area (Rectangle (3.0, 4.0))   (* 12. *)

Каждый конструктор хранит свой набор полей. Невозможно случайно обратиться к «ширине круга» — у круга её просто нет в типе.

Моделирование домена

Алгебраические типы — это способ «сделать недопустимые состояния невыразимыми»:

(* Хорошо: каждый способ несёт ровно свои данные *)
type payment =
  | Cash
  | Card of string                  (* номер карты *)
  | Transfer of { iban : string; bic : string }

Нельзя создать «наличную оплату с номером карты» — такого значения попросту не существует. Это прагматичная сила OCaml: домен описывается типами, а компилятор следит за согласованностью.

Рекурсивные типы

type tree =
  | Leaf
  | Node of int * tree * tree

let rec sum = function
  | Leaf -> 0
  | Node (v, l, r) -> v + sum l + sum r

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

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

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

  • Имя конструктора со строчной буквы. Конструкторы обязаны начинаться с заглавной.
  • Моделировать варианты строками-флагами. Это теряет проверки компилятора.
  • Игнорировать предупреждение о неисчерпывающем match. Это сигнал о реальной дыре в логике.

Итоги

  • Вариантный тип описывает «или то, или то»; конструкторы пишутся с заглавной и могут нести данные.
  • Алгебраические типы делают недопустимые состояния невыразимыми — точное моделирование домена.
  • Закрытость вариантов даёт проверку исчерпывающности при сопоставлении с образцом.
Проверьте себя
1. Что описывает вариантный тип (sum type)?
AЗначение, объединяющее несколько полей одновременно
BЗначение, которое может быть одной из нескольких альтернатив
CТолько перечисление без данных
DИзменяемую структуру
2. С какой буквы должны начинаться конструкторы вариантов?
AСтрочной
BЗаглавной
CС подчёркивания
DС цифры
3. В чём преимущество моделирования домена алгебраическими типами?
AКод короче
BНедопустимые состояния становятся невыразимыми, компилятор проверяет исчерпывающность
CПрограмма быстрее компилируется
DНе нужны функции