Размеченные объединения (discriminated unions)
Размеченные объединения — способ сказать «значение бывает одним из нескольких видов», основа функционального моделирования.
Размеченное объединение (discriminated union, DU) — тип, значение которого относится ровно к одному из перечисленных вариантов, причём каждый вариант может нести свои данные.
Тип-сумма vs тип-произведение
Запись — это «И»: Person = имя и возраст (тип-произведение). DU — это «ИЛИ»: значение — это вариант A или B или C (тип-сумма). Вместе они образуют алгебраические типы данных.
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
| PointShape — это круг (с радиусом), или прямоугольник (с двумя сторонами), или точка (без данных). Невозможно создать «круг с шириной» — структура запрещает бессмыслицу.
Создание и разбор
Значения создают по имени варианта, а разбирают через match — и компилятор проверит, что покрыты все варианты.
type Shape =
| Circle of float
| Rectangle of float * float
| Point
let area shape =
match shape with
| Circle r -> 3.14159 * r * r
| Rectangle (w, h) -> w * h
| Point -> 0.0
printfn "%.2f" (area (Circle 2.0))
printfn "%.2f" (area (Rectangle (3.0, 4.0)))Вывод:
12.57 12.00
Если добавить новый вариант в Shape и забыть обработать его в area, компилятор предупредит — рефакторинг становится безопасным.
Моделирование состояний
DU отлично описывают конечные наборы состояний — там, где в C# использовали бы enum плюс разрозненные поля.
type Payment =
| Cash
| Card of number: string
| Online of provider: string * id: int
let describe p =
match p with
| Cash -> "наличные"
| Card num -> sprintf "карта %s" num
| Online (prov, id) -> sprintf "%s #%d" prov id
printfn "%s" (describe (Online ("PayPal", 42)))Вывод:
PayPal #42
Рекурсивные объединения
DU могут ссылаться на себя — так описывают деревья и выражения.
type Tree =
| Leaf of int
| Node of Tree * Tree
let rec sum t =
match t with
| Leaf v -> v
| Node (l, r) -> sum l + sum r
printfn "%d" (sum (Node (Leaf 1, Node (Leaf 2, Leaf 3))))Вывод:
6
Как работает под капотом
DU компилируется в иерархию классов: базовый тип и по подклассу на каждый вариант, плюс скрытый тег (целое), определяющий вариант. match читает этот тег и извлекает данные подходящего варианта. Это эффективно и типобезопасно: нельзя обратиться к данным не того варианта, минуя match.
Частые ошибки
- Игнорировать предупреждение о неполном
match— это пропущенный вариант, потенциальный баг. - Моделировать «или» через nullable-поля вместо DU — теряется гарантия корректности.
- Забывать данные варианта:
Cardбезof string, когда номер нужен.
Итоги
- DU — тип-сумма: значение относится ровно к одному из вариантов.
- Каждый вариант может нести свои данные (или не нести).
- Разбор через
matchс проверкой исчерпывающности. - DU описывают состояния, выбор и рекурсивные структуры (деревья, выражения).