scalaadvanced

Phantom Types for Compile-Time Safety

Use phantom types to encode state and constraints at the type level without runtime cost.

scala
// Phantom type markers (never instantiated)
sealed trait Locked
sealed trait Unlocked

sealed trait Unverified
sealed trait Verified

// Door that can only be opened when unlocked
class Door[State] private (val name: String):
  override def toString: String = s"Door($name)"

object Door:
  def locked(name: String): Door[Locked] = Door(name)

  extension (door: Door[Locked])
    def unlock: Door[Unlocked] =
      println(s"  Unlocking ${door.name}")
      Door(door.name)

  extension (door: Door[Unlocked])
    def lock: Door[Locked] =
      println(s"  Locking ${door.name}")
      Door(door.name)
    def open: String =
      s"  Opening ${door.name}"

// Email that must be verified before sending
case class Email[Status] private (to: String, subject: String, body: String)

object Email:
  def draft(to: String, subject: String, body: String): Email[Unverified] =
    Email(to, subject, body)

  extension (email: Email[Unverified])
    def verify: Either[String, Email[Verified]] =
      if email.to.contains("@") && email.subject.nonEmpty then
        Right(Email(email.to, email.subject, email.body))
      else Left("Invalid email")

  extension (email: Email[Verified])
    def send: String = s"Sent to ${email.to}: ${email.subject}"

// Builder with phantom types
sealed trait HasHost
sealed trait HasPort
sealed trait NeedsHost
sealed trait NeedsPort

class ServerConfig[H, P] private (
  val host: String,
  val port: Int,
  val ssl: Boolean
):
  override def toString = s"$host:$port (ssl=$ssl)"

object ServerConfig:
  def builder: ServerConfig[NeedsHost, NeedsPort] =
    ServerConfig("", 0, false)

  extension (b: ServerConfig[NeedsHost, NeedsPort])
    def withHost(h: String): ServerConfig[HasHost, NeedsPort] =
      ServerConfig(h, b.port, b.ssl)

  extension [P](b: ServerConfig[HasHost, P])
    def withSSL: ServerConfig[HasHost, P] =
      ServerConfig(b.host, b.port, true)

  extension (b: ServerConfig[HasHost, NeedsPort])
    def withPort(p: Int): ServerConfig[HasHost, HasPort] =
      ServerConfig(b.host, p, b.ssl)

  // Only buildable when both host and port are set
  extension (b: ServerConfig[HasHost, HasPort])
    def build: String = s"Server ready at ${b.host}:${b.port} (ssl=${b.ssl})"

@main def run(): Unit =
  // Door: must unlock before opening
  val door = Door.locked("Front")
  // door.open  // Won't compile! Door is Locked
  val unlocked = door.unlock
  println(unlocked.open)
  val relocked = unlocked.lock
  // relocked.open  // Won't compile again!

  // Email: must verify before sending
  val draft = Email.draft("alice@test.com", "Hello", "World")
  // draft.send  // Won't compile! Unverified
  draft.verify match
    case Right(verified) => println(verified.send)
    case Left(err) => println(s"Error: $err")

  // Invalid email
  val bad = Email.draft("not-email", "", "body")
  println(bad.verify)  // Left

  // Builder: must set host and port
  val config = ServerConfig.builder
    .withHost("0.0.0.0")
    .withSSL
    .withPort(443)
    .build
  println(config)

  // This won't compile — port not set:
  // ServerConfig.builder.withHost("localhost").build

Use Cases

  • State machine enforcement at compile time
  • Ensuring required operations before use
  • Type-safe builder patterns

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.