Type-Level Programming in Scala: Advanced Type System Features
Type-level programming is one of Scala's most powerful features, allowing developers to encode complex constraints, business rules, and computations directly in the type system. This ensures compile-time safety and prevents entire categories of runtime errors. In this comprehensive lesson, we'll explore advanced type-level programming techniques that make your code more robust and expressive.
Understanding Type-Level Programming
Type-level programming involves using types to encode information that is checked at compile time rather than runtime. This approach can prevent bugs, encode business rules, and create more maintainable code.
Basic Type-Level Concepts
// Type-level values using literal types (Scala 3)
val userName: "admin" = "admin"
val port: 8080 = 8080
// Singleton types
object Database
type DatabaseType = Database.type
// Phantom types - types that carry information but no runtime data
sealed trait UserPermission
object UserPermission {
sealed trait Admin extends UserPermission
sealed trait User extends UserPermission
sealed trait Guest extends UserPermission
}
case class SecureResource[P <: UserPermission](data: String)
// Type-level boolean
sealed trait Bool
object Bool {
sealed trait True extends Bool
sealed trait False extends Bool
}
// Type-level natural numbers
sealed trait Nat
object Nat {
sealed trait Zero extends Nat
sealed trait Succ[N <: Nat] extends Nat
type One = Succ[Zero]
type Two = Succ[One]
type Three = Succ[Two]
type Four = Succ[Three]
}
// Type-level lists
sealed trait HList
object HList {
sealed trait HNil extends HList
sealed trait ::[H, T <: HList] extends HList
type StringIntList = String :: Int :: HNil
type BooleanDoubleStringList = Boolean :: Double :: String :: HNil
}
// Compile-time computations with match types (Scala 3)
type Size[L <: HList] <: Nat = L match {
case HNil => Nat.Zero
case _ :: t => Nat.Succ[Size[t]]
}
type Head[L <: HList] = L match {
case h :: _ => h
}
type Tail[L <: HList] <: HList = L match {
case _ :: t => t
}
// Usage examples
val example1: Size[String :: Int :: HNil] = ??? // Type is Nat.Two
val example2: Head[Boolean :: String :: HNil] = true // Type is Boolean
Phantom Types for Compile-Time Safety
Phantom types carry type information without runtime representation, enabling compile-time checks for business rules.
// File system permissions using phantom types
sealed trait Permission
object Permission {
sealed trait Read extends Permission
sealed trait Write extends Permission
sealed trait Execute extends Permission
}
case class File[P <: Permission](path: String, content: String)
// Operations that require specific permissions
object FileOperations {
def read[P <: Permission](file: File[P])(implicit ev: P <:< Permission.Read): String =
file.content
def write[P <: Permission](file: File[P], newContent: String)(implicit ev: P <:< Permission.Write): File[P] =
file.copy(content = newContent)
def execute[P <: Permission](file: File[P])(implicit ev: P <:< Permission.Execute): Unit =
println(s"Executing ${file.path}")
// Grant additional permissions
def grantRead[P <: Permission](file: File[P]): File[P with Permission.Read] =
File(file.path, file.content)
def grantWrite[P <: Permission](file: File[P]): File[P with Permission.Write] =
File(file.path, file.content)
def grantExecute[P <: Permission](file: File[P]): File[P with Permission.Execute] =
File(file.path, file.content)
}
// Usage
val readOnlyFile: File[Permission.Read] = File("readme.txt", "Hello World")
val readWriteFile: File[Permission.Read with Permission.Write] =
FileOperations.grantWrite(readOnlyFile)
val content = FileOperations.read(readOnlyFile) // Compiles
val updated = FileOperations.write(readWriteFile, "Updated content") // Compiles
// FileOperations.write(readOnlyFile, "new") // Compilation error!
// Database connection states using phantom types
sealed trait ConnectionState
object ConnectionState {
sealed trait Connected extends ConnectionState
sealed trait Disconnected extends ConnectionState
sealed trait InTransaction extends ConnectionState
}
case class DatabaseConnection[S <: ConnectionState](connectionString: String)
object Database {
def connect(connectionString: String): DatabaseConnection[ConnectionState.Connected] =
DatabaseConnection(connectionString)
def disconnect[S <: ConnectionState](conn: DatabaseConnection[S]): DatabaseConnection[ConnectionState.Disconnected] =
DatabaseConnection(conn.connectionString)
def beginTransaction(conn: DatabaseConnection[ConnectionState.Connected]): DatabaseConnection[ConnectionState.InTransaction] =
DatabaseConnection(conn.connectionString)
def commit(conn: DatabaseConnection[ConnectionState.InTransaction]): DatabaseConnection[ConnectionState.Connected] =
DatabaseConnection(conn.connectionString)
def rollback(conn: DatabaseConnection[ConnectionState.InTransaction]): DatabaseConnection[ConnectionState.Connected] =
DatabaseConnection(conn.connectionString)
def execute[S <: ConnectionState](conn: DatabaseConnection[S], query: String)(implicit ev: S <:< ConnectionState.Connected): String = {
s"Executed: $query"
}
}
// Usage enforces correct connection state
val conn1 = Database.connect("localhost:5432")
val result1 = Database.execute(conn1, "SELECT * FROM users") // OK
val conn2 = Database.beginTransaction(conn1)
val result2 = Database.execute(conn2, "INSERT INTO users VALUES (...)") // OK
val conn3 = Database.commit(conn2)
val conn4 = Database.disconnect(conn3)
// Database.execute(conn4, "SELECT 1") // Compilation error!
Type-Level Computations
// Type-level arithmetic using match types
type Add[A <: Nat, B <: Nat] <: Nat = A match {
case Nat.Zero => B
case Nat.Succ[a] => Nat.Succ[Add[a, B]]
}
type Multiply[A <: Nat, B <: Nat] <: Nat = A match {
case Nat.Zero => Nat.Zero
case Nat.Succ[a] => Add[B, Multiply[a, B]]
}
// Type-level comparison
type IsEqual[A <: Nat, B <: Nat] <: Bool = (A, B) match {
case (Nat.Zero, Nat.Zero) => Bool.True
case (Nat.Succ[a], Nat.Succ[b]) => IsEqual[a, b]
case _ => Bool.False
}
type LessThan[A <: Nat, B <: Nat] <: Bool = (A, B) match {
case (Nat.Zero, Nat.Succ[_]) => Bool.True
case (Nat.Succ[a], Nat.Succ[b]) => LessThan[a, b]
case _ => Bool.False
}
// Vectors with compile-time length checking
case class Vec[A, N <: Nat](elements: List[A]) {
require(elements.length == natToInt(summon[ValueOf[N]]))
def +[M <: Nat](other: Vec[A, M]): Vec[A, Add[N, M]] =
Vec(elements ++ other.elements)
def head(implicit ev: LessThan[Nat.Zero, N] =:= Bool.True): A =
elements.head
def tail(implicit ev: LessThan[Nat.Zero, N] =:= Bool.True): Vec[A, Nat.Zero] =
Vec(elements.tail)
}
object Vec {
def empty[A]: Vec[A, Nat.Zero] = Vec(List.empty)
def single[A](element: A): Vec[A, Nat.One] = Vec(List(element))
def apply[A](e1: A, e2: A): Vec[A, Nat.Two] = Vec(List(e1, e2))
def apply[A](e1: A, e2: A, e3: A): Vec[A, Nat.Three] = Vec(List(e1, e2, e3))
}
// Helper function to convert type-level nat to runtime int
def natToInt[N <: Nat](n: ValueOf[N]): Int = ??? // Implementation omitted for brevity
// Matrix with compile-time dimension checking
case class Matrix[A, Rows <: Nat, Cols <: Nat](data: List[List[A]]) {
def multiply[NewCols <: Nat](other: Matrix[A, Cols, NewCols]): Matrix[A, Rows, NewCols] = {
// Matrix multiplication implementation
val result = for {
row <- data
newCol <- other.data.transpose
} yield {
// Dot product calculation would go here
newCol.head // Simplified
}
Matrix(result.grouped(natToInt(summon[ValueOf[NewCols]])).toList)
}
def transpose: Matrix[A, Cols, Rows] =
Matrix(data.transpose)
}
// Type-safe SQL query builder
sealed trait SqlType
object SqlType {
sealed trait Integer extends SqlType
sealed trait Text extends SqlType
sealed trait Boolean extends SqlType
sealed trait Timestamp extends SqlType
}
// Column definition with type information
case class Column[T <: SqlType](name: String)
// Table schema using HList-style type-level list
sealed trait Schema
object Schema {
sealed trait Empty extends Schema
sealed trait ::[C <: Column[_], S <: Schema] extends Schema
type UserSchema = Column[SqlType.Integer] :: Column[SqlType.Text] :: Column[SqlType.Text] :: Empty
}
case class Table[S <: Schema](name: String)
// Query builder with compile-time column validation
case class SelectQuery[S <: Schema](table: Table[S], selectedColumns: List[String]) {
def where[T <: SqlType](column: Column[T], value: String)(implicit ev: HasColumn[S, column.type]): SelectQuery[S] =
copy(selectedColumns = selectedColumns) // Simplified
def orderBy[T <: SqlType](column: Column[T])(implicit ev: HasColumn[S, column.type]): SelectQuery[S] =
copy(selectedColumns = selectedColumns) // Simplified
}
// Type-level predicate to check if schema contains column
type HasColumn[S <: Schema, C] <: Bool = S match {
case Schema.Empty => Bool.False
case c :: rest => IsEqual[c, C] match {
case Bool.True => Bool.True
case Bool.False => HasColumn[rest, C]
}
}
// Usage
val userIdCol = Column[SqlType.Integer]("user_id")
val userNameCol = Column[SqlType.Text]("user_name")
val userEmailCol = Column[SqlType.Text]("user_email")
val usersTable: Table[Schema.::] = ??? // Type would be inferred
val query = SelectQuery(usersTable, List("user_id", "user_name"))
.where(userIdCol, "123") // OK
.orderBy(userNameCol) // OK
// .where(Column[SqlType.Text]("invalid_column"), "value") // Compilation error!
Dependent Types and Path-Dependent Types
// Path-dependent types in Scala
trait Container {
type ElementType
def elements: List[ElementType]
def add(element: ElementType): Container { type ElementType = Container.this.ElementType }
}
class StringContainer extends Container {
type ElementType = String
var elements: List[String] = List.empty
def add(element: String): StringContainer = {
elements = element :: elements
this
}
}
class IntContainer extends Container {
type ElementType = Int
var elements: List[Int] = List.empty
def add(element: Int): IntContainer = {
elements = element :: elements
this
}
}
// Dependent method types
trait Processor {
def process[T](input: T): T match {
case String => String
case Int => Int
case List[t] => List[t]
case _ => String
}
}
object DefaultProcessor extends Processor {
def process[T](input: T): T match {
case String => String
case Int => Int
case List[t] => List[t]
case _ => String
} = input match {
case s: String => s.toUpperCase
case i: Int => i * 2
case l: List[t] => l.reverse
case other => other.toString
}
}
// Advanced dependent types with singleton types
class Database {
case class Table(name: String) {
case class Column(name: String, table: Table.this.type = Table.this)
}
val users = Table("users")
val posts = Table("posts")
}
val db = new Database
val userIdColumn = db.users.Column("id")
val postIdColumn = db.posts.Column("id")
// Type-safe foreign key relationships
trait ForeignKey[From <: db.Table, To <: db.Table] {
def fromColumn: From#Column
def toColumn: To#Column
}
// Compile-time validation of data structures
trait Validator[T] {
def validate(value: T): Either[String, T]
}
// Generic validation using type classes
trait ValidationRule[T, R] {
def apply(value: T): Either[String, R]
}
object ValidationRule {
implicit val stringNonEmpty: ValidationRule[String, String] =
(value: String) => if (value.nonEmpty) Right(value) else Left("String cannot be empty")
implicit val intPositive: ValidationRule[Int, Int] =
(value: Int) => if (value > 0) Right(value) else Left("Int must be positive")
implicit val emailFormat: ValidationRule[String, String] =
(value: String) => if (value.contains("@")) Right(value) else Left("Invalid email format")
}
// Compile-time validated data structures
case class ValidatedUser[NameR, EmailR, AgeR](
name: NameR,
email: EmailR,
age: AgeR
)
object ValidatedUser {
def create[NameR, EmailR, AgeR](
name: String,
email: String,
age: Int
)(implicit
nameRule: ValidationRule[String, NameR],
emailRule: ValidationRule[String, EmailR],
ageRule: ValidationRule[Int, AgeR]
): Either[List[String], ValidatedUser[NameR, EmailR, AgeR]] = {
val nameResult = nameRule.apply(name)
val emailResult = emailRule.apply(email)
val ageResult = ageRule.apply(age)
(nameResult, emailResult, ageResult) match {
case (Right(n), Right(e), Right(a)) => Right(ValidatedUser(n, e, a))
case _ =>
val errors = List(nameResult, emailResult, ageResult).collect {
case Left(error) => error
}
Left(errors)
}
}
}
Type-Level State Machines
// Compile-time state machine for order processing
sealed trait OrderState
object OrderState {
sealed trait Created extends OrderState
sealed trait Validated extends OrderState
sealed trait Paid extends OrderState
sealed trait Shipped extends OrderState
sealed trait Delivered extends OrderState
sealed trait Cancelled extends OrderState
}
case class Order[S <: OrderState](
id: String,
items: List[String],
amount: BigDecimal
)
// Valid state transitions encoded in types
trait StateTransition[From <: OrderState, To <: OrderState]
object StateTransition {
implicit val createdToValidated: StateTransition[OrderState.Created, OrderState.Validated] =
new StateTransition[OrderState.Created, OrderState.Validated] {}
implicit val validatedToPaid: StateTransition[OrderState.Validated, OrderState.Paid] =
new StateTransition[OrderState.Validated, OrderState.Paid] {}
implicit val paidToShipped: StateTransition[OrderState.Paid, OrderState.Shipped] =
new StateTransition[OrderState.Paid, OrderState.Shipped] {}
implicit val shippedToDelivered: StateTransition[OrderState.Shipped, OrderState.Delivered] =
new StateTransition[OrderState.Shipped, OrderState.Delivered] {}
implicit val createdToCancelled: StateTransition[OrderState.Created, OrderState.Cancelled] =
new StateTransition[OrderState.Created, OrderState.Cancelled] {}
implicit val validatedToCancelled: StateTransition[OrderState.Validated, OrderState.Cancelled] =
new StateTransition[OrderState.Validated, OrderState.Cancelled] {}
implicit val paidToCancelled: StateTransition[OrderState.Paid, OrderState.Cancelled] =
new StateTransition[OrderState.Paid, OrderState.Cancelled] {}
}
// Order operations that enforce valid state transitions
object OrderOperations {
def validate[From <: OrderState, To <: OrderState](order: Order[From])(
implicit transition: StateTransition[From, To]
): Order[To] =
Order(order.id, order.items, order.amount)
def pay[From <: OrderState, To <: OrderState](order: Order[From])(
implicit transition: StateTransition[From, To]
): Order[To] =
Order(order.id, order.items, order.amount)
def ship[From <: OrderState, To <: OrderState](order: Order[From])(
implicit transition: StateTransition[From, To]
): Order[To] =
Order(order.id, order.items, order.amount)
def deliver[From <: OrderState, To <: OrderState](order: Order[From])(
implicit transition: StateTransition[From, To]
): Order[To] =
Order(order.id, order.items, order.amount)
def cancel[From <: OrderState, To <: OrderState](order: Order[From])(
implicit transition: StateTransition[From, To]
): Order[To] =
Order(order.id, order.items, order.amount)
}
// Usage - only valid transitions compile
val created: Order[OrderState.Created] = Order("1", List("item1"), BigDecimal(100))
val validated: Order[OrderState.Validated] = OrderOperations.validate(created)
val paid: Order[OrderState.Paid] = OrderOperations.pay(validated)
val shipped: Order[OrderState.Shipped] = OrderOperations.ship(paid)
val delivered: Order[OrderState.Delivered] = OrderOperations.deliver(shipped)
// This would cause compilation error:
// val invalidTransition = OrderOperations.ship(created) // Error!
// Type-safe configuration using phantom types
sealed trait ConfigurationKey
object ConfigurationKey {
sealed trait DatabaseUrl extends ConfigurationKey
sealed trait ApiKey extends ConfigurationKey
sealed trait ServerPort extends ConfigurationKey
}
case class Configuration[K <: ConfigurationKey](value: String)
object Configuration {
def databaseUrl(url: String): Configuration[ConfigurationKey.DatabaseUrl] =
Configuration(url)
def apiKey(key: String): Configuration[ConfigurationKey.ApiKey] =
Configuration(key)
def serverPort(port: Int): Configuration[ConfigurationKey.ServerPort] =
Configuration(port.toString)
}
// Application requiring specific configuration types
class Application(
dbUrl: Configuration[ConfigurationKey.DatabaseUrl],
apiKey: Configuration[ConfigurationKey.ApiKey],
port: Configuration[ConfigurationKey.ServerPort]
) {
def start(): Unit = {
println(s"Starting app with DB: ${dbUrl.value}, API Key: ${apiKey.value}, Port: ${port.value}")
}
}
// Compile-time verification of configuration completeness
val app = new Application(
Configuration.databaseUrl("localhost:5432"),
Configuration.apiKey("secret-key"),
Configuration.serverPort(8080)
)
// Mixing up configuration types would cause compilation error
Type-Level Programming for DSLs
// Type-safe HTML DSL using phantom types
sealed trait HtmlTag
object HtmlTag {
sealed trait Div extends HtmlTag
sealed trait Span extends HtmlTag
sealed trait P extends HtmlTag
sealed trait A extends HtmlTag
sealed trait Img extends HtmlTag
}
sealed trait HtmlAttribute
object HtmlAttribute {
sealed trait Id extends HtmlAttribute
sealed trait Class extends HtmlAttribute
sealed trait Href extends HtmlAttribute
sealed trait Src extends HtmlAttribute
sealed trait Alt extends HtmlAttribute
}
case class HtmlElement[T <: HtmlTag](
tagName: String,
attributes: Map[String, String] = Map.empty,
children: List[HtmlElement[_]] = List.empty,
text: Option[String] = None
)
// Valid attribute constraints for different tags
trait ValidAttribute[T <: HtmlTag, A <: HtmlAttribute]
object ValidAttribute {
implicit val divId: ValidAttribute[HtmlTag.Div, HtmlAttribute.Id] = ???
implicit val divClass: ValidAttribute[HtmlTag.Div, HtmlAttribute.Class] = ???
implicit val spanId: ValidAttribute[HtmlTag.Span, HtmlAttribute.Id] = ???
implicit val spanClass: ValidAttribute[HtmlTag.Span, HtmlAttribute.Class] = ???
implicit val aHref: ValidAttribute[HtmlTag.A, HtmlAttribute.Href] = ???
implicit val imgSrc: ValidAttribute[HtmlTag.Img, HtmlAttribute.Src] = ???
implicit val imgAlt: ValidAttribute[HtmlTag.Img, HtmlAttribute.Alt] = ???
}
// HTML builder with compile-time validation
object Html {
def div: HtmlElement[HtmlTag.Div] = HtmlElement("div")
def span: HtmlElement[HtmlTag.Span] = HtmlElement("span")
def p: HtmlElement[HtmlTag.P] = HtmlElement("p")
def a: HtmlElement[HtmlTag.A] = HtmlElement("a")
def img: HtmlElement[HtmlTag.Img] = HtmlElement("img")
implicit class HtmlElementOps[T <: HtmlTag](element: HtmlElement[T]) {
def withId(id: String)(implicit ev: ValidAttribute[T, HtmlAttribute.Id]): HtmlElement[T] =
element.copy(attributes = element.attributes + ("id" -> id))
def withClass(className: String)(implicit ev: ValidAttribute[T, HtmlAttribute.Class]): HtmlElement[T] =
element.copy(attributes = element.attributes + ("class" -> className))
def withHref(href: String)(implicit ev: ValidAttribute[T, HtmlAttribute.Href]): HtmlElement[T] =
element.copy(attributes = element.attributes + ("href" -> href))
def withSrc(src: String)(implicit ev: ValidAttribute[T, HtmlAttribute.Src]): HtmlElement[T] =
element.copy(attributes = element.attributes + ("src" -> src))
def withAlt(alt: String)(implicit ev: ValidAttribute[T, HtmlAttribute.Alt]): HtmlElement[T] =
element.copy(attributes = element.attributes + ("alt" -> alt))
def withText(text: String): HtmlElement[T] =
element.copy(text = Some(text))
def withChildren(children: HtmlElement[_]*): HtmlElement[T] =
element.copy(children = children.toList)
}
}
// Usage - only valid combinations compile
val validHtml = Html.div
.withId("main-content")
.withClass("container")
.withChildren(
Html.p.withText("Welcome to our site"),
Html.a.withHref("https://example.com").withText("Visit our homepage"),
Html.img.withSrc("logo.png").withAlt("Company logo")
)
// This would cause compilation errors:
// Html.img.withHref("invalid") // Error: img cannot have href
// Html.p.withSrc("invalid") // Error: p cannot have src
// Type-safe SQL DSL
sealed trait SqlExpression
sealed trait SqlColumn extends SqlExpression
sealed trait SqlTable extends SqlExpression
case class Table[Schema](name: String) extends SqlTable
case class Column[T, TableType <: SqlTable](name: String, table: TableType) extends SqlColumn
sealed trait SqlOperator
object SqlOperator {
sealed trait Equals extends SqlOperator
sealed trait GreaterThan extends SqlOperator
sealed trait LessThan extends SqlOperator
sealed trait In extends SqlOperator
}
case class Condition[T, Op <: SqlOperator](
column: Column[T, _],
operator: Op,
value: T
)
case class SelectQuery[T <: SqlTable](
table: T,
columns: List[Column[_, T]] = List.empty,
conditions: List[Condition[_, _]] = List.empty
) {
def select[ColType](column: Column[ColType, T]): SelectQuery[T] =
copy(columns = column :: columns)
def where[ColType, Op <: SqlOperator](
column: Column[ColType, T],
operator: Op,
value: ColType
): SelectQuery[T] =
copy(conditions = Condition(column, operator, value) :: conditions)
}
// Usage
val usersTable = Table[Unit]("users")
val userIdColumn = Column[Int, Table[Unit]]("id", usersTable)
val userNameColumn = Column[String, Table[Unit]]("name", usersTable)
val query = SelectQuery(usersTable)
.select(userIdColumn)
.select(userNameColumn)
.where(userIdColumn, SqlOperator.Equals, 123)
.where(userNameColumn, SqlOperator.Equals, "John")
Type-Level Testing and Verification
// Compile-time tests using shapeless-style techniques
trait =:=[A, B]
object =:= {
implicit def refl[A]: A =:= A = new =:=[A, A] {}
}
// Type-level assertions
def assertType[Expected, Actual](implicit ev: Expected =:= Actual): Unit = ()
// Test type-level computations
object TypeLevelTests {
// Test natural number addition
assertType[Nat.Two, Add[Nat.One, Nat.One]]
assertType[Nat.Four, Add[Nat.Two, Nat.Two]]
// Test boolean operations
assertType[Bool.True, IsEqual[Nat.One, Nat.One]]
assertType[Bool.False, IsEqual[Nat.One, Nat.Two]]
// Test HList size calculation
assertType[Nat.Zero, Size[HList.HNil]]
assertType[Nat.Two, Size[String :: Int :: HList.HNil]]
// These would cause compilation errors if types don't match:
// assertType[Nat.Three, Add[Nat.One, Nat.One]] // Error!
// assertType[Bool.True, IsEqual[Nat.One, Nat.Two]] // Error!
}
// Property-based testing for type-level properties
trait TypeProperty[P[_]] {
def verify[T]: P[T] => Unit
}
// Example: proving that adding zero is identity
trait AddZeroIdentity[N <: Nat] {
def proof: Add[N, Nat.Zero] =:= N
}
object AddZeroIdentity {
implicit def zeroCase: AddZeroIdentity[Nat.Zero] =
new AddZeroIdentity[Nat.Zero] {
def proof: Add[Nat.Zero, Nat.Zero] =:= Nat.Zero = implicitly
}
implicit def succCase[N <: Nat](implicit
ih: AddZeroIdentity[N]
): AddZeroIdentity[Nat.Succ[N]] =
new AddZeroIdentity[Nat.Succ[N]] {
def proof: Add[Nat.Succ[N], Nat.Zero] =:= Nat.Succ[N] = implicitly
}
}
// Type-level proofs and theorems
trait Theorem[Statement] {
def proof: Statement
}
// Commutativity of addition (simplified)
trait AddCommutative[A <: Nat, B <: Nat] extends Theorem[Add[A, B] =:= Add[B, A]]
// Associativity of addition (simplified)
trait AddAssociative[A <: Nat, B <: Nat, C <: Nat] extends Theorem[Add[Add[A, B], C] =:= Add[A, Add[B, C]]]
// Runtime verification of type-level constraints
class TypeSafeContainer[T, Constraint[_]](private val value: T)(implicit constraint: Constraint[T]) {
def get: T = value
def map[U](f: T => U)(implicit newConstraint: Constraint[U]): TypeSafeContainer[U, Constraint] =
new TypeSafeContainer(f(value))
def flatMap[U](f: T => TypeSafeContainer[U, Constraint]): TypeSafeContainer[U, Constraint] =
f(value)
}
// Example constraints
trait NonEmpty[T]
object NonEmpty {
implicit val stringNonEmpty: NonEmpty[String] = new NonEmpty[String] {}
implicit def listNonEmpty[A]: NonEmpty[List[A]] = new NonEmpty[List[A]] {}
}
trait Positive[T]
object Positive {
implicit val intPositive: Positive[Int] = new Positive[Int] {}
implicit val doublePositive: Positive[Double] = new Positive[Double] {}
}
// Usage
val nonEmptyString = new TypeSafeContainer("hello")(NonEmpty.stringNonEmpty)
val positiveInt = new TypeSafeContainer(42)(Positive.intPositive)
// Compile-time documentation through types
/**
* A function that processes user input and returns a validated result.
* The type signature documents the constraints and guarantees.
*/
def processUserInput[
Input <: String,
Output <: String
](input: Input)(implicit
inputConstraint: NonEmpty[Input],
outputConstraint: NonEmpty[Output]
): Either[String, TypeSafeContainer[Output, NonEmpty]] = {
if (input.trim.nonEmpty) {
val processed = input.toUpperCase.asInstanceOf[Output]
Right(new TypeSafeContainer(processed))
} else {
Left("Input cannot be empty")
}
}
Advanced Patterns and Best Practices
Type-Level Design Patterns
// Typeclass pattern for type-level programming
trait TypeLevelShow[T] {
type Result <: String
def show: Result
}
object TypeLevelShow {
type Aux[T, R <: String] = TypeLevelShow[T] { type Result = R }
implicit def natShow[N <: Nat]: Aux[N, String] =
new TypeLevelShow[N] {
type Result = String
def show: String = "Nat" // Simplified
}
implicit def boolShow[B <: Bool]: Aux[B, String] =
new TypeLevelShow[B] {
type Result = String
def show: String = "Bool" // Simplified
}
}
// Phantom evidence pattern
sealed trait Evidence[Statement]
object Evidence {
def apply[Statement](implicit ev: Evidence[Statement]): Evidence[Statement] = ev
// Provide evidence for true statements
implicit def natIsNat[N <: Nat]: Evidence[N <:< Nat] = new Evidence[N <:< Nat] {}
implicit def boolIsBool[B <: Bool]: Evidence[B <:< Bool] = new Evidence[B <:< Bool] {}
}
// Type-level constraints composition
trait AllConstraints[T, C1[_], C2[_], C3[_]] {
def constraint1: C1[T]
def constraint2: C2[T]
def constraint3: C3[T]
}
object AllConstraints {
implicit def all[T](implicit
c1: C1[T],
c2: C2[T],
c3: C3[T]
): AllConstraints[T, C1, C2, C3] =
new AllConstraints[T, C1, C2, C3] {
def constraint1: C1[T] = c1
def constraint2: C2[T] = c2
def constraint3: C3[T] = c3
}
}
// Refinement types using dependent types
object Refinements {
type PositiveInt = Int
type NonEmptyString = String
type ValidEmail = String
def positive(n: Int): Option[PositiveInt] =
if (n > 0) Some(n) else None
def nonEmpty(s: String): Option[NonEmptyString] =
if (s.nonEmpty) Some(s) else None
def validEmail(s: String): Option[ValidEmail] =
if (s.contains("@")) Some(s) else None
// Smart constructors with type-level guarantees
case class Person private (
name: NonEmptyString,
email: ValidEmail,
age: PositiveInt
)
object Person {
def create(name: String, email: String, age: Int): Either[String, Person] = {
for {
validName <- nonEmpty(name).toRight("Name cannot be empty")
validEmail <- validEmail(email).toRight("Invalid email format")
validAge <- positive(age).toRight("Age must be positive")
} yield Person(validName, validEmail, validAge)
}
}
}
// Type-level configuration validation
trait ConfigValidation[Config] {
def validate(config: Config): Either[List[String], Config]
}
case class DatabaseConfig(
host: String,
port: Int,
database: String,
username: String,
password: String
)
case class ServerConfig(
port: Int,
maxConnections: Int,
timeout: Int
)
case class AppConfig(
database: DatabaseConfig,
server: ServerConfig,
debugMode: Boolean
)
object ConfigValidation {
implicit val databaseConfigValidation: ConfigValidation[DatabaseConfig] =
(config: DatabaseConfig) => {
val errors = List.newBuilder[String]
if (config.host.isEmpty) errors += "Database host cannot be empty"
if (config.port <= 0 || config.port > 65535) errors += "Database port must be between 1 and 65535"
if (config.database.isEmpty) errors += "Database name cannot be empty"
if (config.username.isEmpty) errors += "Database username cannot be empty"
val errorList = errors.result()
if (errorList.isEmpty) Right(config) else Left(errorList)
}
implicit val serverConfigValidation: ConfigValidation[ServerConfig] =
(config: ServerConfig) => {
val errors = List.newBuilder[String]
if (config.port <= 0 || config.port > 65535) errors += "Server port must be between 1 and 65535"
if (config.maxConnections <= 0) errors += "Max connections must be positive"
if (config.timeout <= 0) errors += "Timeout must be positive"
val errorList = errors.result()
if (errorList.isEmpty) Right(config) else Left(errorList)
}
implicit val appConfigValidation: ConfigValidation[AppConfig] =
(config: AppConfig) => {
for {
validDatabase <- implicitly[ConfigValidation[DatabaseConfig]].validate(config.database)
validServer <- implicitly[ConfigValidation[ServerConfig]].validate(config.server)
} yield config.copy(database = validDatabase, server = validServer)
}
}
// Type-safe builder pattern using phantom types
sealed trait BuilderState
object BuilderState {
sealed trait Empty extends BuilderState
sealed trait HasName extends BuilderState
sealed trait HasEmail extends BuilderState
sealed trait HasAge extends BuilderState
sealed trait Complete extends BuilderState
}
case class PersonBuilder[S <: BuilderState] private (
name: Option[String] = None,
email: Option[String] = None,
age: Option[Int] = None
)
object PersonBuilder {
def apply(): PersonBuilder[BuilderState.Empty] =
new PersonBuilder[BuilderState.Empty]()
implicit class EmptyBuilderOps(builder: PersonBuilder[BuilderState.Empty]) {
def withName(name: String): PersonBuilder[BuilderState.HasName] =
PersonBuilder(Some(name), builder.email, builder.age)
}
implicit class HasNameBuilderOps(builder: PersonBuilder[BuilderState.HasName]) {
def withEmail(email: String): PersonBuilder[BuilderState.HasEmail] =
PersonBuilder(builder.name, Some(email), builder.age)
}
implicit class HasEmailBuilderOps(builder: PersonBuilder[BuilderState.HasEmail]) {
def withAge(age: Int): PersonBuilder[BuilderState.Complete] =
PersonBuilder(builder.name, builder.email, Some(age))
}
implicit class CompleteBuilderOps(builder: PersonBuilder[BuilderState.Complete]) {
def build(): Either[String, Person] = {
for {
name <- builder.name.toRight("Name is required")
email <- builder.email.toRight("Email is required")
age <- builder.age.toRight("Age is required")
person <- Refinements.Person.create(name, email, age)
} yield person
}
}
}
// Usage - builder enforces correct construction order
val person = PersonBuilder()
.withName("John Doe")
.withEmail("john@example.com")
.withAge(30)
.build()
// This would cause compilation errors:
// PersonBuilder().withEmail("john@example.com") // Error: must set name first
// PersonBuilder().withName("John").build() // Error: incomplete builder
Performance Considerations
Type-level programming is resolved at compile time, but there are still performance considerations:
// Compile-time optimization strategies
object OptimizedTypeLevelCode {
// Use type aliases to reduce compilation time
type UserId = String
type UserEmail = String
type UserName = String
// Prefer simple type constraints over complex type computations
trait ValidUser[U] {
def isValid: Boolean
}
object ValidUser {
implicit def stringUserId: ValidUser[UserId] =
new ValidUser[UserId] { def isValid = true }
}
// Cache expensive type-level computations
object TypeCache {
type StringIntList = String :: Int :: HList.HNil
type BoolDoubleList = Boolean :: Double :: HList.HNil
// Pre-computed common sizes
type Size0 = Nat.Zero
type Size1 = Nat.One
type Size2 = Nat.Two
type Size3 = Nat.Three
}
// Use implicit classes for extension methods instead of complex inheritance
implicit class SafeListOps[T](list: List[T]) {
def safeHead: Option[T] = list.headOption
def safeTail: List[T] = if (list.nonEmpty) list.tail else List.empty
}
// Minimize type-level recursion depth
type LimitedSize[L <: HList] <: Nat = L match {
case HList.HNil => Nat.Zero
case _ :: HList.HNil => Nat.One
case _ :: _ :: HList.HNil => Nat.Two
case _ :: _ :: _ :: HList.HNil => Nat.Three
case _ => Nat.Four // Cap at 4 for performance
}
}
Conclusion
Type-level programming in Scala provides powerful tools for encoding complex constraints and business rules directly in the type system. Key benefits include:
Compile-Time Safety:
- Prevent entire categories of runtime errors
- Encode business rules as type constraints
- Verify correctness at compile time
Advanced Type System Features:
- Phantom types for zero-cost abstractions
- Dependent types for complex relationships
- Type-level computations for compile-time verification
- Match types for type-level pattern matching
Practical Applications:
- State machines with compile-time validation
- DSLs with type-safe construction
- Configuration validation
- Protocol implementations
- Resource management
Design Patterns:
- Specification pattern for business rules
- Builder pattern with compile-time validation
- Type-safe configuration management
- Refinement types for constrained values
Best Practices:
- Keep type-level code simple and readable
- Use type aliases to improve clarity
- Cache expensive type computations
- Minimize recursion depth for compilation performance
- Document type-level constraints clearly
Testing and Verification:
- Compile-time assertions for type properties
- Type-level proofs and theorems
- Property-based testing for type invariants
- Documentation through expressive types
Type-level programming enables the creation of APIs that are not only safe but also guide developers toward correct usage through the type system. When used judiciously, these techniques can dramatically improve code quality and maintainability while providing excellent developer experience through compile-time feedback.
Remember that with great power comes great responsibility - use type-level programming to solve real problems and improve safety, not just to show off the type system's capabilities. The goal is always to make code more maintainable, safe, and expressive.
Comments
Be the first to comment on this lesson!