Skip to content
AgileLogics
6 min read

Enums and Algebraic Data Types

Prerequisites: Traits, Case classes

Simple enums

enum Color:
  case Red, Green, Blue

enum Planet(val massKg: Double, val radiusM: Double):
  case Mercury extends Planet(3.303e+23, 2.4397e6)
  case Earth   extends Planet(5.976e+24, 6.37814e6)

  def surfaceGravity: Double = 6.67300e-11 * massKg / (radiusM * radiusM)

@main def enums(): Unit =
  println(Color.Green)
  println(Color.values.toList)
  println(Color.valueOf("Blue").ordinal)
  println(f"${Planet.Earth.surfaceGravity}%.2f m/s²")
Green
List(Red, Green, Blue)
2
9.80 m/s²

Enums can have parameters, fields, and methods; values, valueOf, and ordinal come for free.

Algebraic data types

Enum cases can also carry different data — making an algebraic data type (ADT): a type that is exactly one of several shapes.

enum Payment:
  case Card(number: String, expiry: String)
  case BankTransfer(iban: String)
  case Cash

def describe(p: Payment): String = p match
  case Payment.Card(number, _)     => s"card ending ${number.takeRight(4)}"
  case Payment.BankTransfer(iban)  => s"transfer from $iban"
  case Payment.Cash                => "cash"

@main def payments(): Unit =
  val payments = List(
    Payment.Card("4111111111111111", "12/27"),
    Payment.BankTransfer("GB33BUKB20201555555555"),
    Payment.Cash
  )
  payments.map(describe).foreach(println)
card ending 1111
transfer from GB33BUKB20201555555555
cash

The compiler checks every case

Because the set of cases is closed, the compiler warns when a match misses one:

enum Light:
  case Red, Amber, Green

def next(light: Light): Light = light match
  case Light.Red   => Light.Green
  case Light.Green => Light.Amber

@main def lights(): Unit =
  println(next(Light.Red))
-- [E029] Pattern Match Exhaustivity Warning: Main.scala:4:32 ------------------
4 |def next(light: Light): Light = light match
  |                                ^^^^^
  |                                match may not be exhaustive.
  |
  |                                It would fail on pattern case: Amber
  |
  | longer explanation available when compiling with `-explain`
1 warning found
Green

Add a new case to an enum, and the compiler points to every match that needs updating. This exhaustiveness checking is one of the biggest benefits of modeling data with ADTs.

Sealed traits

Before Scala 3, ADTs were written as a sealed trait with case classes — still common, and still useful when cases need their own methods or hierarchies:

sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(side: Double) extends Shape

def area(s: Shape): Double = s match
  case Circle(r)    => math.Pi * r * r
  case Square(side) => side * side

@main def sealedTraits(): Unit =
  println(area(Square(3)))
9.0

sealed means all subtypes must be in the same file, so the compiler knows the complete list.