Sealed-иерархии и enum

Слово sealed даёт компилятору суперсилу: он начинает следить, не забыли ли вы обработать какой-то случай.

«Запечатанная иерархия — это обещание: все варианты известны заранее, сюрпризов не будет.»

Часто данные имеют ограниченный набор форм: светофор — красный, жёлтый или зелёный; результат — успех или ошибка. Чтобы выразить «вот все возможные варианты, и других не будет», используют sealed.

sealed trait Light
case object Red extends Light
case object Yellow extends Light
case object Green extends Light

Ключевое слово sealed означает: все наследники этого трейта объявлены в этом же файле. Компилятор знает их все наперечёт — и может проверять, что вы обработали каждый случай в паттерн-матчинге (увидим в следующем разделе).

enum — современный способ

В Scala 3 для перечислений появился специальный enum — куда короче, чем sealed trait с объектами.

enum Light:
  case Red, Yellow, Green

val signal = Light.Green
println(signal)            // Green
println(Light.values.toList)  // List(Red, Yellow, Green)

Параметризованный enum

enum может нести данные — это уже алгебраический тип данных (ADT). Каждый вариант может иметь свои поля.

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

import Shape.*
val s: Shape = Circle(2.0)
val r: Shape = Rectangle(3.0, 4.0)

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

# В Python для перечислений есть Enum, для ADT — классы/dataclass
from enum import Enum

class Light(Enum):
    RED = 1
    YELLOW = 2
    GREEN = 3

signal = Light.GREEN
print(signal.name)            # GREEN
print([l.name for l in Light])  # ['RED', 'YELLOW', 'GREEN']

# ADT с данными — через dataclass-варианты
from dataclasses import dataclass
@dataclass
class Circle: radius: float
@dataclass
class Rectangle:
    w: float
    h: float
sealed trait Light
   |        |        |
  Red    Yellow   Green   <- ВСЕ варианты известны

компилятор: "в match обработай каждый, иначе предупрежу"

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

enum в Scala 3 — это синтаксический сахар над sealed-иерархией. Простые случаи (Red, Green) компилируются в синглтоны-объекты, а варианты с данными (Circle(radius)) — в case-классы. Компилятор также генерирует методы вроде values и valueOf. Поскольку всё запечатано, компилятор во время проверки типов держит полный список наследников и сверяет с ним match-выражения.

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

  • Объявить наследника sealed-трейта в другом файле. Это запрещено — теряется гарантия полноты.
  • Использовать обычный класс там, где нужен закрытый набор вариантов. Без sealed компилятор не проверит полноту.
  • Путать enum-варианты без данных и с данными. case Red — синглтон, case Circle(r) — конструктор.

Best practices

  • Для закрытого набора вариантов в Scala 3 предпочитайте enum — он лаконичнее.
  • Используйте sealed, чтобы получать предупреждения о необработанных случаях.
  • Моделируйте предметную область как ADT: «данные — это сумма вариантов».

Моделирование данных как суммы вариантов

Запечатанные иерархии и enum открывают важный приём — моделирование предметной области как алгебраических типов данных. Идея проста: многие сущности — это «одно из нескольких». Платёж — наличными, картой или переводом. Сообщение — текст, картинка или файл. Выражая это через закрытый набор вариантов, вы делаете структуру данных самодокументируемой и защищённой компилятором.

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

Привыкайте начинать моделирование данных с вопроса «сколько у этой сущности возможных форм и все ли они известны заранее?». Если форм конечное число и они известны, перед вами кандидат на enum или sealed-иерархию. Этот вопрос, заданный в начале проектирования, экономит часы отладки потом, потому что компилятор берёт на себя контроль полноты.

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

Проверьте себя
1. Что даёт ключевое слово sealed для трейта?
AЗапрещает создавать объекты
BГарантирует, что все наследники объявлены в этом же файле, и позволяет проверять полноту в match
CДелает трейт быстрее
DШифрует данные
2. Какой синтаксис добавлен в Scala 3 для перечислений?
Asealed class
Benum
Cinterface
Drecord