Lesson 9 / 25

Case Classes, Enums and ADTs

Model data with case classes, enums and sealed hierarchies.

Algebraic data types

Case classes are concise immutable data types: case class Item(sku: String, qty: Int) generates public val fields, equals/hashCode by value, a readable toString, a copy method for modified copies and pattern-matching support. Enums (Scala 3) define a fixed set of cases, either simple (enum Status { case Placed, Paid, Shipped }) or with parameters and methods, and they interoperate with Java enums when extending java.lang.Enum. Together they express algebraic data types (ADTs): product types (a case class holds this and that) and sum types (an enum or sealed trait is this or that). For example, a payment is Card(last4) or Upi(vpa) or Cash. Because the set of cases is closed, the compiler warns about non-exhaustive matches, so adding a new case shows you every place to update. Modelling the domain precisely with ADTs makes illegal states unrepresentable: rather than a status: String with a nullable trackingId, use Shipped(trackingId) that only exists once shipped.

Modelling payments and order states

Enums with data, exhaustive matches and copy.

enum Payment:
  case Card(last4: String)
  case Upi(vpa: String)
  case Cash

enum OrderState:
  case Placed
  case Paid(payment: Payment)
  case Shipped(payment: Payment, trackingId: String)
  case Cancelled(reason: String)

case class Order(id: String, totalPaise: Long, state: OrderState)

def describe(p: Payment): String = p match
  case Payment.Card(last4) => s"card ending $last4"
  case Payment.Upi(vpa)    => s"UPI $vpa"
  case Payment.Cash        => "cash on delivery"
  // removing a case here makes the compiler warn: match may not be exhaustive

val placed = Order("o-1", 49900, OrderState.Placed)
val paid = placed.copy(state = OrderState.Paid(Payment.Upi("asha@okbank")))
val shipped = paid.state match
  case OrderState.Paid(p) => paid.copy(state = OrderState.Shipped(p, "TRK123"))
  case _                  => paid

println(shipped)
println(placed == Order("o-1", 49900, OrderState.Placed))   // true: value equality

values exists only for simple enums

EnumName.values and valueOf are generated only when every case is a simple singleton such as enum Status { case Placed, Paid }. Payment and OrderState have parameterised cases, so they do not get values.

Quick check: What does making a hierarchy sealed (or using an enum) enable?

  • Faster runtime dispatch only
  • Mutable fields
  • Compiler exhaustiveness checks in pattern matches, because all cases are known
  • Multiple inheritance of classes
Answer

Compiler exhaustiveness checks in pattern matches, because all cases are known — A closed set of cases lets the compiler warn when a match misses one.