Enums and algebraic data types

Scala · Practical · 14 min

ADTs let the compiler prove you've handled every case, eliminating whole classes of bugs.

An algebraic data type (ADT) is a type defined as one of a fixed set of alternatives, each possibly carrying data. Scala 3's enum expresses them directly:

  • A simple enumeration: enum Color { case Red, Green, Blue } with Color.values, Color.valueOf("Red") and ordinal.
  • Enums can have parameters and members: enum Planet(val mass: Double) { case Earth extends Planet(5.97e24) ... }.
  • Cases can carry their own data, making a full ADT: enum Shape { case Circle(r: Double); case Rect(w: Double, h: Double) }.
  • Enums can be generic: the standard Option could be written enum Option[+A] { case Some(x: A); case None }.

The equivalent in Scala 2 style, still common and sometimes more flexible, is a sealed trait with case class and case object subtypes in the same file.

Because the set of cases is closed, a match that misses one produces a compiler warning ("match may not be exhaustive"). Treat it as an error (-Werror) and you get a compile-time checklist whenever a new case is added.

Design tip: make illegal states unrepresentable. Instead of a class with nullable fields and a status string, define an enum where each case has exactly the data that state needs.

Example

enum Color:
  case Red, Green, Blue

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

import Payment.*

def fee(p: Payment, amount: Double): Double = p match
  case Card(_, _)  => amount * 0.02
  case Transfer(_) => 0.5
  case Cash        => 0.0
  // removing a case gives: "match may not be exhaustive"

@main def enums(): Unit =
  println(Color.values.toList)             // List(Red, Green, Blue)
  println(Color.valueOf("Green").ordinal)  // 1
  val payments = List(Card("4242", "12/29"), Transfer("AM00"), Cash)
  println(payments.map(fee(_, 100)))       // List(2.0, 0.5, 0.0)
  println(Transfer("X"))                   // Transfer(X)

Remember

  • enum models a closed set of cases, with or without data
  • Sealed traits plus case classes are the equivalent older encoding
  • Exhaustiveness checking warns when a case is missing
  • Make illegal states unrepresentable

Watch out for

  • Adding a wildcard case _ to every match, which silences exhaustiveness warnings when new cases are added.

Check yourself

  1. Why prefer an enum over a String status field?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/scala3/book/domain-modeling-fp.html