Scala Algebraic Data Types
Algebraic Data Types (ADTs) are a way to model data precisely using combinations of two building blocks: sum types (a value is one of several variants) and product types (a value holds several fields together). In Scala, sealed traits create sum types and case classes create product types. Together they let you model complex domains with types that are exhaustive, composable, and pattern-matchable.
Product Types — AND
A product type combines multiple fields. A Person has a name AND an age AND an email. Case classes are product types:
case class Person(name: String, age: Int, email: String)
// A Person = name AND age AND email
case class Point(x: Double, y: Double)
// A Point = x AND y
The number of possible values is the product of each field's possibilities — hence the name "product type."
Sum Types — OR
A sum type is a value that can be exactly one of several variants. A Shape is a Circle OR a Rectangle OR a Triangle — not all three at once:
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, h: Double) extends Shape
case class Triangle(base: Double, h: Double) extends Shape
// A Shape = Circle OR Rectangle OR Triangle
SUM TYPE (OR): PRODUCT TYPE (AND):
──────────────────────────────── ────────────────────────────────
sealed trait Shape case class Circle(radius: Double)
Circle OR Rectangle OR Triangle = one Double (the radius)
One variant at a time All fields present simultaneously
Combining ADTs
// A payment can be paid by Card OR Cash OR BankTransfer
sealed trait PaymentMethod
case class Card(last4: String, expiry: String) extends PaymentMethod
case class Cash(amount: Double) extends PaymentMethod
case class BankTransfer(accountNo: String, ifsc: String) extends PaymentMethod
// An order status is one of these variants
sealed trait OrderResult
case class Success(orderId: String, method: PaymentMethod) extends OrderResult
case class Failure(code: Int, reason: String) extends OrderResult
def processOrder(result: OrderResult): Unit = result match
case Success(id, Card(last4, exp)) =>
println(s"Order $id paid by card ending $last4 (exp: $exp)")
case Success(id, Cash(amount)) =>
println(f"Order $id paid by cash: ₹$amount%.2f")
case Success(id, BankTransfer(acc, ifsc)) =>
println(s"Order $id via bank transfer (${acc.take(4)}xxxx, $ifsc)")
case Failure(code, reason) =>
println(s"Order failed [$code]: $reason")
processOrder(Success("ORD001", Card("4532", "12/26")))
// Order ORD001 paid by card ending 4532 (exp: 12/26)
processOrder(Failure(402, "Insufficient funds"))
// Order failed [402]: Insufficient funds
Recursive ADTs — Trees
ADTs can be recursive. A binary tree is either a Leaf or a Node that contains two sub-trees:
sealed trait Tree[+A]
case object Leaf extends Tree[Nothing]
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]
// 5
// / \
// 3 8
// / \ \
// 1 4 9
val tree: Tree[Int] =
Node(5,
Node(3, Node(1, Leaf, Leaf), Node(4, Leaf, Leaf)),
Node(8, Leaf, Node(9, Leaf, Leaf))
)
def sum(t: Tree[Int]): Int = t match
case Leaf => 0
case Node(v, l, r) => v + sum(l) + sum(r)
def depth(t: Tree[_]): Int = t match
case Leaf => 0
case Node(_, l, r) => 1 + (depth(l) max depth(r))
println(sum(tree)) // 30
println(depth(tree)) // 3
Tree[Int]
│
├── Leaf (base case)
└── Node(value, left, right) (recursive case)
Expression ADT
sealed trait Expr
case class Num(value: Double) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Mul(left: Expr, right: Expr) extends Expr
case class Neg(expr: Expr) extends Expr
def eval(e: Expr): Double = e match
case Num(v) => v
case Add(l, r) => eval(l) + eval(r)
case Mul(l, r) => eval(l) * eval(r)
case Neg(x) => -eval(x)
// Represents: (3 + 4) * -(2 + 1)
val expr = Mul(Add(Num(3), Num(4)), Neg(Add(Num(2), Num(1))))
println(eval(expr)) // -21.0
def show(e: Expr): String = e match
case Num(v) => v.toString
case Add(l, r) => s"(${show(l)} + ${show(r)})"
case Mul(l, r) => s"(${show(l)} * ${show(r)})"
case Neg(x) => s"-(${show(x)})"
println(show(expr)) // ((3.0 + 4.0) * -((2.0 + 1.0)))
ADTs in Domain Modeling
sealed trait UserStatus
case object Active extends UserStatus
case object Suspended extends UserStatus
case class Banned(reason: String, until: Option[String]) extends UserStatus
case class User(
id: Int,
name: String,
email: String,
status: UserStatus
)
def canLogin(user: User): Boolean = user.status match
case Active => true
case Suspended => false
case Banned(_, _) => false
def statusMessage(user: User): String = user.status match
case Active => s"Welcome, ${user.name}!"
case Suspended => "Your account is suspended. Contact support."
case Banned(r, Some(d)) => s"Banned until $d: $r"
case Banned(r, None) => s"Permanently banned: $r"
val u1 = User(1, "Alice", "a@b.com", Active)
val u2 = User(2, "Bob", "b@b.com", Banned("Policy violation", Some("2025-01-01")))
println(statusMessage(u1)) // Welcome, Alice!
println(statusMessage(u2)) // Banned until 2025-01-01: Policy violation
println(canLogin(u1)) // true
println(canLogin(u2)) // false
ADTs make illegal states unrepresentable. A user cannot be both Active and Banned. A shape cannot be both a Circle and a Rectangle. The type system enforces these constraints at compile time, eliminating entire categories of runtime bugs.
