Алгебраические типы данных (ADT)

Алгебраические типы данных (ADT) могут быть созданы с помощью конструкции enum.

Концепция enum является достаточно общей, чтобы также поддерживать алгебраические типы данных (ADT) и их обобщенную версию (GADT). Вот пример, показывающий, как тип Option может быть представлен в виде ADT:

enum Option[+T]:
  case Some(x: T)
  case None

В этом примере создается Option enum с параметром ковариантного типа T, состоящим из двух вариантов: Some и None. Some параметризуются параметром значения x; это сокращение для написания case класса, который расширяет Option. Поскольку None не параметризован, он обрабатывается как обычное значение enum.

Предложения extends, которые были опущены в предыдущем примере, также могут быть указаны явно:

enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[Nothing]

Как и в случае с обычными значениями enum, case enum определяются в сопутствующем объекте перечисления, поэтому они называются Option.Some и Option.None (если только определения не "вытягиваются" при импорте):

Option.Some("hello")
// res0: Option[String] = Some(x = "hello")
Option.None
// res1: Option[Nothing] = None

Обратите внимание, что тип приведенных выше выражений всегда Option. Как правило, тип case enum будет расширен до базового типа enum, если не ожидается более конкретный тип. Это тонкая разница по сравнению с обычными case class-ами. Классы, составляющие case enum, существуют, и их можно использовать, либо создав их непосредственно с помощью new, либо явно указав ожидаемый тип.

new Option.Some(2)
// res2: Some[Int] = Some(x = 2)
val x: Option.Some[Int] = Option.Some(3)
// x: Some[Int] = Some(x = 3)

Как и в других случаях использования перечисления, АТД могут определять дополнительные методы. Например, вот снова Option с методом isDefined и конструктором Option(...) в сопутствующем объекте:

enum Option[+T]:
  case Some(x: T)
  case None

  def isDefined: Boolean = this match
    case None => false
    case Some(_) => true

object Option:
  def apply[T >: Null](x: T): Option[T] =
    if (x == null) None else Some(x)

Перечисления и ADT используют одну и ту же синтаксическую конструкцию, поэтому их можно рассматривать просто как два конца спектра, и вполне возможно создавать гибриды. Например, приведенный ниже код дает реализацию Color либо с тремя значениями перечисления, либо с параметризованным case, который принимает значение RGB:

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)
  case Mix(mix: Int) extends Color(mix)
Рекурсивные перечисления

До сих пор все перечисления состояли только из различных вариантов значений или case class-ов. Перечисления также могут быть рекурсивными, как показано в приведенном ниже примере кодирования натуральных чисел:

enum Nat:
  case Zero
  case Succ(n: Nat)

Например, значение Succ(Succ(Zero)) представляет число 2 в унарной кодировке. Очень похожим образом могут быть определены списки:

enum List[+A]:
  case Nil
  case Cons(head: A, tail: List[A])

Обобщенные алгебраические типы данных (GADT)

Приведенная выше нотация для перечислений очень лаконична и служит идеальной отправной точкой для моделирования типов данных. Также возможно выразить гораздо более мощные типы: обобщенные алгебраические типы данных (GADTs).

Вот пример GADT, где параметр типа (T) определяет содержимое, хранящееся в поле:

enum Box[T](contents: T):
  case IntBox(n: Int) extends Box[Int](n)
  case BoolBox(b: Boolean) extends Box[Boolean](b)

Сопоставление с шаблоном в конкретном конструкторе (IntBox или BoolBox) восстанавливает информацию о типе:

def extract[T](b: Box[T]): T = b match
  case IntBox(n)  => n + 1
  case BoolBox(b) => !b

Безопасно возвращать Int только в первом случае, так как из сопоставления с шаблоном известно, что b был IntBox.

Дешугаризация перечислений

Концептуально перечисления можно рассматривать как определение sealed класса вместе с сопутствующим объектом. Посмотрим на дешугаризацию перечисления Color:

sealed abstract class Color(val rgb: Int) extends scala.reflect.Enum
object Color:
  case object Red extends Color(0xFF0000) { def ordinal = 0 }
  case object Green extends Color(0x00FF00) { def ordinal = 1 }
  case object Blue extends Color(0x0000FF) { def ordinal = 2 }
  case class Mix(mix: Int) extends Color(mix) { def ordinal = 3 }

  def fromOrdinal(ordinal: Int): Color = ordinal match
    case 0 => Red
    case 1 => Green
    case 2 => Blue
    case _ => throw new NoSuchElementException(ordinal.toString)

Вышеописанная дешугаризация упрощена, детали доступны по ссылке.

Хотя перечисления можно кодировать вручную с помощью других конструкций, использование enum является более кратким, а также включает несколько дополнительных утилит (таких как метод fromOrdinal).


Ссылки: