Системы типов реальных языков и продвинутые возможности
Как теория приземляется в индустрию: что выбрали реальные языки и какие мощные механизмы существуют.
Дизайн системы типов — всегда компромисс между выразительностью (сколько ошибок ловим), автоматизмом вывода и простотой для человека.
Компромиссы реальных языков
| Язык | Ставка | Цена |
| Haskell | Хиндли — Милнер + классы типов, чистота | сложные расширения, ленивость путает |
| Rust | владение и заимствование как типы, без GC | крутая кривая обучения, borrow checker строг |
| TypeScript | структурная типизация поверх JS, постепенность | лазейка any, нестрогая по умолчанию |
| Scala | ООП + ФП, подтипы + дженерики + impliciit | сложность, медленный вывод в углах |
| OCaml | быстрый ML-вывод, прагматизм | меньше классов типов (модули вместо) |
Видна закономерность: чем мощнее система (Haskell, Rust), тем выше порог входа; чем удобнее (TypeScript), тем больше дыр в гарантиях. Идеала нет — есть точка на оси «гарантии — удобство», выбранная под задачи языка.
GADT: обобщённые алгебраические типы
Обычный тип-сумма даёт всем конструкторам один и тот же результирующий тип. GADT позволяет каждому конструктору уточнять параметр типа. Это даёт типобезопасный интерпретатор: выражение Expr a, где a — тип результата, и IntLit строит Expr Int, а сравнение — Expr Bool. Матчинг по конструктору уточняет тип в ветке — «забыть» обработать тип невозможно.
data Expr a where
IntLit :: Int -> Expr Int
BoolLit:: Bool -> Expr Bool
Add :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
-- eval :: Expr a -> a -- результат правильного типа гарантирован
Higher-kinded types и линейные типы
Higher-kinded types (HKT) — абстракция над конструкторами типов, а не только типами. Functor f работает для любого f вида Type -> Type (списки, Maybe, деревья). Это позволяет писать map, Monad единообразно. Поддерживают Haskell, Scala; Rust и TypeScript — лишь частично или обходными путями.
Линейные типы требуют, чтобы значение использовалось ровно один раз. На них держится система владения Rust: borrow и перемещение — это линейность, проверяемая компилятором, дающая безопасность памяти без сборщика мусора. Эффекты и сессионные типы — родственные идеи: тип кодирует, какие побочные действия и в каком порядке допустимы.
Демонстрация: типобезопасный интерпретатор (идея GADT)
Смоделируем выражения с тегом типа результата и интерпретатор, гарантирующий правильный тип. Каждый узел знает свой тип результата — как уточнённый GADT-конструктор.
# узлы помечены типом результата: ("int", v) -> Int, ("bool", v) -> Bool
def IntLit(n): return ("int_lit", n)
def BoolLit(b): return ("bool_lit", b)
def Add(x, y): return ("add", x, y) # Expr Int -> Expr Int -> Expr Int
def If(c, t, e): return ("if", c, t, e)
def eval(e):
tag = e[0]
if tag == "int_lit": return e[1]
if tag == "bool_lit": return e[1]
if tag == "add": return eval(e[1]) + eval(e[2])
if tag == "if": return eval(e[2]) if eval(e[1]) else eval(e[3])
expr = If(BoolLit(True), Add(IntLit(2), IntLit(3)), IntLit(0))
print("eval (if true then 2+3 else 0):", eval(expr))
print("eval (5 + 7) :", eval(Add(IntLit(5), IntLit(7))))
print("в GADT тип Expr a гарантирует: результат именно типа a")
Вывод:
eval (if true then 2+3 else 0): 5 eval (5 + 7) : 12 в GADT тип Expr a гарантирует: результат именно типа a
В настоящем GADT компилятор не дал бы сложить BoolLit с IntLit и гарантировал бы тип результата eval; наш Python лишь имитирует идею динамически.
Как работает под капотом
GADT реализуются через равенства типов (type equality constraints), которые конструктор «приносит» в ветку матчинга: сопоставив IntLit, компилятор узнаёт a ~ Int и использует это локально. HKT требуют системы видов (kinds) — «типов для типов»: Int :: Type, Maybe :: Type -> Type. Линейные типы добавляют в систему учёт кратности использования переменной. Все три — расширения базового скелета правил вывода.
Частые ошибки
- Считать
anyв TypeScript «типом». Это выход из системы типов; он отключает гарантии в своей области. - Путать GADT с обычной суммой. GADT уточняет параметр результата по конструктору — обычная сумма так не умеет.
- Думать, что borrow checker Rust — особый механизм. Это применение линейных/affine-типов: значение используется ограниченное число раз.
Итоги
- Реальные языки выбирают точку на оси «гарантии — удобство»; идеала нет.
- GADT уточняет тип результата по конструктору — типобезопасные интерпретаторы.
- HKT абстрагируют над конструкторами типов (Functor, Monad) через систему видов.
- Линейные типы (владение Rust), эффекты, сессионные типы кодируют «как и когда» использовать значение.