Scala 3 — Book

Algebraic Data Types

Language
This doc page is specific to Scala 3, and may cover new concepts not available in Scala 2. Unless otherwise stated, all the code examples in this page assume you are using Scala 3.

Algebraic Data Types (ADTs) can be created with the enum construct, so we’ll briefly review enumerations before looking at ADTs.

Enumerations

An enumeration is used to define a type consisting of a set of named values:

enum Color:
  case Red, Green, Blue

which can be seen as a shorthand for:

enum Color:
  case Red   extends Color
  case Green extends Color
  case Blue  extends Color

Parameters

Enums can be parameterized:

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)

This way, each of the different variants has a value member rgb which is assigned the corresponding value:

println(Color.Green.rgb) // prints 65280

Custom Definitions

Enums can also have custom definitions:

enum Planet(mass: Double, radius: Double):

  private final val G = 6.67300E-11
  def surfaceGravity = G * mass / (radius * radius)
  def surfaceWeight(otherMass: Double) =  otherMass * surfaceGravity

  case Mercury extends Planet(3.303e+23, 2.4397e6)
  case Venus   extends Planet(4.869e+24, 6.0518e6)
  case Earth   extends Planet(5.976e+24, 6.37814e6)
  // 5 or 6 more planets ...

Like classes and case classes, you can also define a companion object for an enum:

object Planet:
  def main(args: Array[String]) =
    val earthWeight = args(0).toDouble
    val mass = earthWeight / Earth.surfaceGravity
    for (p <- values)
      println(s"Your weight on $p is ${p.surfaceWeight(mass)}")

Algebraic Datatypes (ADTs)

The enum concept is general enough to also support algebraic data types (ADTs) and their generalized version (GADTs). Here’s an example that shows how an Option type can be represented as an ADT:

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

This example creates an Option enum with a covariant type parameter T consisting of two cases, Some and None. Some is parameterized with a value parameter x; this is a shorthand for writing a case class that extends Option. Since None is not parameterized, it’s treated as a normal enum value.

The extends clauses that were omitted in the previous example can also be given explicitly:

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

As with normal enum values, the cases of an enum are defined in the enums companion object, so they’re referred to as Option.Some and Option.None (unless the definitions are “pulled out” with an import):

scala> Option.Some("hello")
val res1: t2.Option[String] = Some(hello)

scala> Option.None
val res2: t2.Option[Nothing] = None

As with other enumeration uses, ADTs can define additional methods. For instance, here’s Option again, with an isDefined method and an Option(...) constructor in its companion object:

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)

Enumerations and ADTs share the same syntactic construct, so they can be seen simply as two ends of a spectrum, and it’s perfectly possible to construct hybrids. For instance, the code below gives an implementation of Color, either with three enum values or with a parameterized case that takes an RGB value:

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)

Recursive Enumerations

So far all the enumerations that we defined consisted of different variants of values or case classes. Enumerations can also be recursive, as illustrated in the below example of encoding natural numbers:

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

For example the value Succ(Succ(Zero)) represents the number 2 in an unary encoding. Lists can be defined in a very similar way:

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

Generalized Algebraic Datatypes (GADTs)

The above notation for enumerations is very concise and serves as the perfect starting point for modeling your data types. Since we can always be more explicit, it is also possible to express types that are much more powerful: generalized algebraic datatypes (GADTs).

Here is an example of a GADT where the type parameter (T) specifies the contents stored in the box:

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

Pattern matching on the particular constructor (IntBox or BoolBox) recovers the type information:

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

It is only safe to return an Int in the first case, since we know from pattern matching that the input was an IntBox.

Desugaring Enumerations

Conceptually, enums can be thought of as defining a sealed class together with its companion object. Let’s look at the desugaring of our Color enum above:

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)

Note that the above desugaring is simplified and we purposefully leave out some details.

While enums could be manually encoded using other constructs, using enumerations is more concise and also comes with a few additional utilities (such as the fromOrdinal method).

Contributors to this page: