Размеченные объединения (discriminated unions)

Размеченные объединения — способ сказать «значение бывает одним из нескольких видов», основа функционального моделирования.

Размеченное объединение (discriminated union, DU) — тип, значение которого относится ровно к одному из перечисленных вариантов, причём каждый вариант может нести свои данные.

Тип-сумма vs тип-произведение

Запись — это «И»: Person = имя и возраст (тип-произведение). DU — это «ИЛИ»: значение — это вариант A или B или C (тип-сумма). Вместе они образуют алгебраические типы данных.

type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Point

Shape — это круг (с радиусом), или прямоугольник (с двумя сторонами), или точка (без данных). Невозможно создать «круг с шириной» — структура запрещает бессмыслицу.

Создание и разбор

Значения создают по имени варианта, а разбирают через 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 описывают состояния, выбор и рекурсивные структуры (деревья, выражения).
Проверьте себя
1. Что моделирует размеченное объединение?
AСразу несколько полей вместе (И)
BОдин из нескольких вариантов (ИЛИ)
CТолько числа
DИзменяемое состояние
2. Что произойдёт, если в match по DU забыть один вариант?
AПрограмма упадёт молча
BКомпилятор предупредит о неполном сопоставлении
CНичего, вариант пропустится
DБудет ошибка только в рантайме
3. Зачем DU могут быть рекурсивными?
AДля ускорения
BЧтобы описывать деревья и выражения, ссылаясь на себя
CЧтобы запретить match
DЭто запрещено