Исчерпывающие проверки sealed-типов

Здесь sealed и паттерн-матчинг встречаются, и компилятор становится вашим напарником по код-ревью.

«Исчерпывающая проверка — это страховка от будущего: добавите новый вариант — компилятор напомнит обработать его везде.»

В прошлом разделе мы видели sealed и enum. Их главная награда раскрывается именно в match: поскольку компилятор знает все варианты, он проверяет, что вы обработали каждый. Пропустили — получите предупреждение.

enum Light:
  case Red, Yellow, Green

import Light.*

def action(l: Light): String =
  l match
    case Red    => "стой"
    case Yellow => "приготовься"
    case Green  => "езжай"
    // если убрать Green — компилятор предупредит!

Это огромное преимущество перед строками или числами как «состояниями»: там компилятор не знает полного списка и молчит.

ADT с данными

Для алгебраических типов с полями проверка работает так же — и деконструкция извлекает данные каждого варианта.

enum Shape:
  case Circle(r: Double)
  case Rectangle(w: Double, h: Double)

import Shape.*

def area(s: Shape): Double =
  s match
    case Circle(r)       => math.Pi * r * r
    case Rectangle(w, h) => w * h

println(area(Circle(2)))         // ~12.57
println(area(Rectangle(3, 4)))   // 12.0

Классический пример: дерево выражений

enum Expr:
  case Num(value: Int)
  case Add(left: Expr, right: Expr)
  case Mul(left: Expr, right: Expr)

import Expr.*

def eval(e: Expr): Int =
  e match
    case Num(v)     => v
    case Add(a, b)  => eval(a) + eval(b)
    case Mul(a, b)  => eval(a) * eval(b)

println(eval(Add(Num(2), Mul(Num(3), Num(4)))))  // 14

Та же идея на Python ▶

# Дерево выражений на Python через dataclass + match
from dataclasses import dataclass

@dataclass
class Num: value: int
@dataclass
class Add:
    left: object
    right: object
@dataclass
class Mul:
    left: object
    right: object

def eval_expr(e):
    match e:
        case Num(value=v):
            return v
        case Add(left=a, right=b):
            return eval_expr(a) + eval_expr(b)
        case Mul(left=a, right=b):
            return eval_expr(a) * eval_expr(b)

print(eval_expr(Add(Num(2), Mul(Num(3), Num(4)))))  # 14
sealed/enum: { Red, Yellow, Green }  <- полный список

match обрабатывает: Red? Yellow? Green?
   пропущен Green  ->  компилятор: "match не исчерпывающий!"

Как работает под капотом (JVM)

Проверка исчерпываемости происходит на этапе компиляции, а не выполнения. Компилятор берёт полный список наследников sealed-типа (он его знает, потому что они все в одном файле) и сверяет с образцами в match. Если какой-то вариант не покрыт и нет case _, выдаётся предупреждение. В байт-код это никак не попадает — это чистая проверка времени компиляции, бесплатная для исполнения.

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

  • Глушить предупреждение через case _. Тогда при добавлении нового варианта компилятор промолчит, и вы забудете его обработать.
  • Использовать не-sealed иерархию. Без sealed компилятор не знает полного списка и не проверяет полноту.
  • Игнорировать предупреждения компилятора. Настройте сборку так, чтобы они были заметны.

Best practices

  • Моделируйте состояния и варианты как enum/sealed, чтобы получать проверку полноты.
  • Избегайте case _ для закрытых иерархий — пусть компилятор следит за новыми вариантами.
  • Включайте режим «предупреждения как ошибки» в серьёзных проектах.

Компилятор как соавтор

Исчерпывающие проверки превращают компилятор в активного участника разработки. Обычно мы думаем о компиляторе как о придирчивом контролёре, который ищет ошибки. Здесь он работает иначе — как соавтор, который помнит все варианты и следит, чтобы вы ничего не упустили. Это особенно ценно в долгоживущих проектах, где определения типов меняются спустя месяцы после написания кода.

Классический пример пользы — дерево выражений или абстрактное синтаксическое дерево. Когда вы добавляете новый вид узла, компилятор пройдётся по всем функциям, которые обходят дерево, и потребует обработать новый случай. Без этой проверки легко забыть одно из мест и получить ошибку времени выполнения у пользователя. С ней рефакторинг становится механическим и безопасным: правь определение, чини все подсвеченные места, готово.

Многие опытные команды специально включают режим, в котором предупреждения компилятора считаются ошибками сборки. Тогда неисчерпывающий match просто не даст собрать проект, пока вы не обработаете все случаи. Это превращает мягкое предупреждение в твёрдую гарантию и делает целый класс ошибок невозможным в продакшене.

Итоги. Для sealed-иерархий и enum компилятор проверяет, что match покрывает все варианты, защищая от забытых случаев при развитии кода. Это завершает раздел про паттерн-матчинг; дальше — коллекции.

Проверьте себя
1. Что сделает компилятор, если в match по enum Light пропустить вариант Green?
AУдалит вариант Green
BВыдаст предупреждение о неисчерпывающем match
CСоздаст Green автоматически
DНичего, это нормально
2. Почему case _ может быть вреден для sealed-иерархии?
AОн замедляет программу
BОн перехватывает всё, и при добавлении нового варианта компилятор не предупредит, что его забыли обработать
CОн запрещён синтаксисом
DОн создаёт бесконечный цикл