Что такое sealed class/trait и зачем она нужна?

В Scala ключевое слово sealed используется для ограничения иерархии наследования классов и трейтов. Оно указывает компилятору, что все подклассы данного класса или трейта должны быть определены в одном и том же исходном файле, что даёт множество преимуществ как в плане безопасности, так и в плане удобства работы с сопоставлением с образцом (pattern matching).

Синтаксис и пример

sealed trait Expr
case class Num(n: Int) extends Expr
case class Add(a: Expr, b: Expr) extends Expr

В этом примере Expr — это sealed trait, и все его возможные реализации (Num, Add) определены в том же файле.

Зачем нужен sealed

1. Полнота (исчерпываемость) сопоставления с образцом

Компилятор знает, какие классы являются всеми возможными подтипами sealed-типа, и может проверять, что match выражение покрывает все случаи.

def eval(expr: Expr): Int = expr match {
case Num(n) => n
case Add(a, b) => eval(a) + eval(b)
// если бы не было sealed, компилятор не знал бы  все ли варианты учтены
}

Если вы забудете вариант — компилятор предупредит.

2. Безопасность типов

sealed помогает построить замкнутую иерархию, которую нельзя расширить вне данного файла. Это особенно важно при проектировании доменной модели или DSL (domain-specific language), где вы хотите контролировать все допустимые варианты значений.

3. Поддержка в IDE и инструментовке

IDE (например, IntelliJ IDEA) может подсказывать все возможные варианты для sealed-типа, автогенерировать match выражения с exhaustiveness checking.

4. Улучшение читаемости и поддержки кода

Программисту не нужно беспокоиться о том, что в другом месте кода кто-то добавит новый подкласс, который нарушит логику обработки. Всё, что может быть — видно на месте.

Отличия от final и abstract

  • final запрещает наследование: нельзя унаследовать или переопределить final-класс.

  • abstract требует, чтобы класс был расширен: он неполный, требует реализации.

  • sealed разрешает наследование, но только внутри одного файла.

Отличие sealed и sealed abstract

Обычно sealed trait используется, но можно писать sealed abstract class, если нужны общие поля или логика.

sealed abstract class Shape(val name: String)
case class Circle(r: Double) extends Shape("circle")
case class Square(s: Double) extends Shape("square")

Расширение иерархии — только внутри одного файла

// Файл: Expr.scala
sealed trait Expr
case class Num(n: Int) extends Expr
case class Add(a: Expr, b: Expr) extends Expr
// В другом файле:
// case class Sub(a: Expr, b: Expr) extends Expr // Ошибка!

Совместимость с enum (начиная с Scala 3)

В Scala 3 enum автоматически является sealed:

enum Expr:
case Num(n: Int)
case Add(a: Expr, b: Expr)

Здесь Expr — sealed, и расширение возможно только в этом файле.

Использование в ADT (алгебраических типах данных)

sealed trait + case class — это стандартная практика моделирования доменной логики в функциональном стиле.

sealed trait Option\[+A\]
case class Some\[A\](value: A) extends Option\[A\]
case object None extends Option\[Nothing\]

Это позволяет задать чёткую структуру значений с безопасной и предсказуемой обработкой через match.

Возможность комбинации с case class

Часто sealed используется вместе с case class или case object, так как эти классы автоматически получают:

  • методы equals, hashCode, toString

  • unapply (для pattern matching)

  • копирование через copy

Уточнение: sealed в Scala 3

В Scala 3 появились модификаторы sealed и open, а также новое ключевое слово enum, но смысл sealed остался прежним — ограничить наследование одним файлом.

Краткие тезисы (без выделения итогов):

  • sealed ограничивает расширение класса или трейта только текущим исходным файлом.

  • Обеспечивает безопасность и контроль иерархии.

  • Позволяет компилятору проверять полноту pattern matching.

  • Часто используется в моделировании алгебраических типов данных.

  • Не позволяет расширять sealed-типы в других файлах проекта.

  • Хорошо сочетается с case class, case object и паттерн-матчингом.