Scala Sealed Traits

A sealed trait restricts which classes or objects can extend it — only those defined in the same file. This gives the compiler complete knowledge of every possible subtype. As a result, the compiler can check that your pattern matches cover every case and warn you when one is missing.

Why sealed?


Without sealed:                  With sealed:
────────────────────────────     ────────────────────────────────
trait Color                      sealed trait Color
// Any file can add subtypes     // Only THIS file can add subtypes

case object Red extends Color    case object Red   extends Color
// Later, in another file:       case object Green extends Color
case object Purple extends Color // case object Blue  extends Color
// Compiler cannot know all
// subtypes — match is unsafe    // Compiler knows: Red, Green, Blue
                                 // Warns if match misses any!

Basic Sealed Trait

sealed trait Direction
case object North extends Direction
case object South extends Direction
case object East  extends Direction
case object West  extends Direction

def opposite(d: Direction): Direction = d match
  case North => South
  case South => North
  case East  => West
  case West  => East
// No _ needed — compiler knows all cases

println(opposite(North))   // South
println(opposite(East))    // West

Sealed Trait with Case Classes

sealed trait Shape
case class Circle(radius: Double)              extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double)  extends Shape

def area(s: Shape): Double = s match
  case Circle(r)       => Math.PI * r * r
  case Rectangle(w, h) => w * h
  case Triangle(b, h)  => 0.5 * b * h

val shapes: List[Shape] = List(Circle(3), Rectangle(4,5), Triangle(6,8))
shapes.foreach(s => println(f"${s.getClass.getSimpleName}: ${area(s)}%.2f"))
// Circle: 28.27
// Rectangle: 20.00
// Triangle: 24.00

sealed trait Shape
      │
      ├── Circle     (case class)
      ├── Rectangle  (case class)
      └── Triangle   (case class)

Compiler knows EXACTLY these 3 subtypes.
Miss one in a match → compile warning.

Sealed Trait for State Machines

sealed trait OrderStatus
case object Pending   extends OrderStatus
case object Confirmed extends OrderStatus
case object Shipped   extends OrderStatus
case object Delivered extends OrderStatus
case class  Cancelled(reason: String) extends OrderStatus

def nextStatus(current: OrderStatus): Option[OrderStatus] = current match
  case Pending           => Some(Confirmed)
  case Confirmed         => Some(Shipped)
  case Shipped           => Some(Delivered)
  case Delivered         => None   // terminal state
  case Cancelled(_)      => None   // terminal state

def statusLabel(s: OrderStatus): String = s match
  case Pending         => "⏳ Awaiting confirmation"
  case Confirmed       => "✅ Order confirmed"
  case Shipped         => "🚚 Out for delivery"
  case Delivered       => "📦 Delivered"
  case Cancelled(r)    => s"❌ Cancelled: $r"

var status: OrderStatus = Pending
for _ <- 1 to 4 do
  println(statusLabel(status))
  status = nextStatus(status).getOrElse(status)

println(statusLabel(status))
// ⏳ Awaiting confirmation
// ✅ Order confirmed
// 🚚 Out for delivery
// 📦 Delivered
// 📦 Delivered  (stays Delivered)

sealed trait vs sealed abstract class

// sealed trait — cannot have constructor parameters (Scala 2)
sealed trait Event

// sealed abstract class — can have constructor parameters
sealed abstract class Event(val timestamp: Long)
case class LoginEvent(userId: Int, override val timestamp: Long)  extends Event(timestamp)
case class LogoutEvent(userId: Int, override val timestamp: Long) extends Event(timestamp)

def handle(e: Event): String = e match
  case LoginEvent(uid, ts)  => s"User $uid logged in at $ts"
  case LogoutEvent(uid, ts) => s"User $uid logged out at $ts"

The Compiler Warning in Action

sealed trait Light
case object Red    extends Light
case object Yellow extends Light
case object Green  extends Light

// Missing Yellow → compiler warning:
// "match may not be exhaustive. It would fail on pattern: Yellow"
def action(l: Light): String = l match
  case Red   => "Stop"
  // case Yellow => "Caution"  ← commented out on purpose
  case Green => "Go"

// With _ fallback — suppresses warning but hides the gap:
def safeAction(l: Light): String = l match
  case Red   => "Stop"
  case Green => "Go"
  case _     => "Unknown"  // ← hides the missing Yellow

The best practice is to handle every case explicitly rather than relying on case _. When you add a new subtype later, the compiler will immediately show you every match that needs updating.

Sealed Traits as Sum Types

Sealed trait hierarchies model what mathematicians call a sum type — a type that is exactly one of its variants. This is perfect for representing concepts that have a finite set of states:

sealed trait Result[+A]
case class  Ok[A](value: A)     extends Result[A]
case class  Err(message: String) extends Result[Nothing]

def safeDivide(a: Int, b: Int): Result[Double] =
  if b == 0 then Err("Division by zero")
  else Ok(a.toDouble / b)

safeDivide(10, 2) match
  case Ok(v)  => println(f"Result: $v%.2f")   // Result: 5.00
  case Err(m) => println(s"Error: $m")

safeDivide(7, 0) match
  case Ok(v)  => println(f"Result: $v%.2f")
  case Err(m) => println(s"Error: $m")         // Error: Division by zero

Leave a Comment

Your email address will not be published. Required fields are marked *