Implicits and Type Classes: Building Elegant APIs

Introduction

Scala's implicit system is one of its most powerful and distinctive features, enabling the creation of elegant APIs, type classes, and domain-specific languages. When used correctly, implicits can make code more readable and expressive while maintaining type safety. However, they require careful understanding to avoid confusion and maintain code clarity.

This lesson will teach you to use implicits effectively, design type class hierarchies, and build APIs that feel natural and intuitive while leveraging the full power of Scala's type system.

Understanding Implicits

Implicit Parameters

// Basic implicit parameters
trait Formatter[T] {
  def format(value: T): String
}

implicit val intFormatter: Formatter[Int] = new Formatter[Int] {
  def format(value: Int): String = s"Number: $value"
}

implicit val stringFormatter: Formatter[String] = new Formatter[String] {
  def format(value: String): String = s"Text: '$value'"
}

implicit val doubleFormatter: Formatter[Double] = new Formatter[Double] {
  def format(value: Double): String = f"Decimal: $value%.2f"
}

// Function using implicit parameter
def display[T](value: T)(implicit formatter: Formatter[T]): String = {
  formatter.format(value)
}

// Usage - implicit parameter is resolved automatically
println(display(42))        // Uses intFormatter
println(display("hello"))   // Uses stringFormatter
println(display(3.14159))   // Uses doubleFormatter

// Context bounds (syntactic sugar for implicit parameters)
def displayWithBound[T : Formatter](value: T): String = {
  implicitly[Formatter[T]].format(value)
}

// Multiple implicit parameters
trait Ordering[T] {
  def compare(x: T, y: T): Int
}

trait Show[T] {
  def show(value: T): String
}

implicit val intOrdering: Ordering[Int] = new Ordering[Int] {
  def compare(x: Int, y: Int): Int = x.compareTo(y)
}

implicit val stringOrdering: Ordering[String] = new Ordering[String] {
  def compare(x: String, y: String): Int = x.compareTo(y)
}

implicit val intShow: Show[Int] = new Show[Int] {
  def show(value: Int): String = value.toString
}

implicit val stringShow: Show[String] = new Show[String] {
  def show(value: String): String = s"\"$value\""
}

def sortAndDisplay[T](items: List[T])(implicit ord: Ordering[T], show: Show[T]): String = {
  val sorted = items.sortWith((a, b) => ord.compare(a, b) < 0)
  sorted.map(show.show).mkString("[", ", ", "]")
}

val numbers = List(3, 1, 4, 1, 5, 9)
val words = List("banana", "apple", "cherry")

println(sortAndDisplay(numbers))
println(sortAndDisplay(words))

// Implicit parameter with default values
trait Config {
  def timeout: Int
  def retries: Int
}

object Config {
  implicit val defaultConfig: Config = new Config {
    def timeout: Int = 5000
    def retries: Int = 3
  }
}

def makeRequest[T](url: String)(implicit config: Config): String = {
  s"Making request to $url with timeout ${config.timeout}ms and ${config.retries} retries"
}

println(makeRequest("https://api.example.com"))

// Custom config
implicit val customConfig: Config = new Config {
  def timeout: Int = 10000
  def retries: Int = 5
}

println(makeRequest("https://api.example.com"))

// Implicit parameter scope and resolution
object Database {
  trait Connection {
    def execute(query: String): String
  }

  implicit val defaultConnection: Connection = new Connection {
    def execute(query: String): String = s"Executing: $query"
  }
}

def runQuery(sql: String)(implicit conn: Database.Connection): String = {
  conn.execute(sql)
}

import Database.defaultConnection
println(runQuery("SELECT * FROM users"))

// Local implicit scope
def withCustomConnection(): String = {
  implicit val customConnection: Database.Connection = new Database.Connection {
    def execute(query: String): String = s"Custom execution: $query"
  }

  runQuery("SELECT * FROM products")  // Uses customConnection
}

println(withCustomConnection())

// Implicit parameters in constructors
class Logger(implicit prefix: String) {
  def log(message: String): Unit = println(s"[$prefix] $message")
}

implicit val logPrefix: String = "APP"

val logger = new Logger()
logger.log("Application started")

// Different implicit scope
def withDifferentPrefix(): Unit = {
  implicit val prefix: String = "TEST"
  val testLogger = new Logger()
  testLogger.log("Running tests")
}

withDifferentPrefix()

// Implicit parameter groups
class ServiceClient(baseUrl: String)(implicit config: Config, conn: Database.Connection) {
  def get(path: String): String = {
    val fullUrl = s"$baseUrl$path"
    s"GET $fullUrl with timeout ${config.timeout}ms using ${conn.execute("connection check")}"
  }
}

val client = new ServiceClient("https://api.service.com")
println(client.get("/users/123"))

Implicit Conversions

// Implicit conversions (use sparingly!)
implicit def stringToInt(s: String): Int = s.toInt

def addNumbers(a: Int, b: Int): Int = a + b

// Implicit conversion allows this to work
val result = addNumbers(10, "20")  // "20" is converted to 20
println(s"Result: $result")

// More practical implicit conversion
case class Temperature(celsius: Double) {
  def toFahrenheit: Double = celsius * 9 / 5 + 32
  def toKelvin: Double = celsius + 273.15
}

implicit def doubleToTemperature(celsius: Double): Temperature = Temperature(celsius)

def printTemperature(temp: Temperature): Unit = {
  println(f"${temp.celsius}%.1f°C = ${temp.toFahrenheit}%.1f°F = ${temp.toKelvin}%.1f K")
}

printTemperature(25.0)  // Double is implicitly converted to Temperature
printTemperature(0.0)   // Implicit conversion
printTemperature(-40.0) // Implicit conversion

// Implicit conversion for collections
implicit def listToRichList[T](list: List[T]): RichList[T] = new RichList(list)

class RichList[T](list: List[T]) {
  def second: Option[T] = list.drop(1).headOption
  def secondLast: Option[T] = list.reverse.drop(1).headOption
  def safely(index: Int): Option[T] = if (index >= 0 && index < list.length) Some(list(index)) else None
}

val numbers2 = List(1, 2, 3, 4, 5)
println(s"Second element: ${numbers2.second}")
println(s"Second last element: ${numbers2.secondLast}")
println(s"Element at index 10: ${numbers2.safely(10)}")

// Implicit conversions for DSL
case class Duration(length: Long, unit: String) {
  def toMillis: Long = unit match {
    case "ms" => length
    case "s" => length * 1000
    case "m" => length * 1000 * 60
    case "h" => length * 1000 * 60 * 60
    case _ => length
  }

  override def toString: String = s"$length $unit"
}

implicit class DurationOps(length: Long) {
  def ms: Duration = Duration(length, "ms")
  def seconds: Duration = Duration(length, "s")
  def minutes: Duration = Duration(length, "m")
  def hours: Duration = Duration(length, "h")
}

// DSL usage
val timeout1 = 30.seconds
val timeout2 = 5.minutes
val timeout3 = 2.hours

println(s"Timeout 1: $timeout1 (${timeout1.toMillis} ms)")
println(s"Timeout 2: $timeout2 (${timeout2.toMillis} ms)")
println(s"Timeout 3: $timeout3 (${timeout3.toMillis} ms)")

// Implicit conversion for enhanced APIs
case class Money(amount: BigDecimal, currency: String) {
  def +(other: Money): Money = {
    require(currency == other.currency, "Cannot add different currencies")
    Money(amount + other.amount, currency)
  }

  def *(multiplier: BigDecimal): Money = Money(amount * multiplier, currency)

  override def toString: String = f"$amount%.2f $currency"
}

implicit class BigDecimalOps(amount: BigDecimal) {
  def USD: Money = Money(amount, "USD")
  def EUR: Money = Money(amount, "EUR")
  def GBP: Money = Money(amount, "GBP")
}

// Enhanced API usage
val price1 = BigDecimal("19.99").USD
val price2 = BigDecimal("5.00").USD
val total = price1 + price2
val withTax = total * BigDecimal("1.08")

println(s"Price 1: $price1")
println(s"Price 2: $price2")
println(s"Total: $total")
println(s"With tax: $withTax")

// Implicit conversions with type safety
sealed trait HttpMethod
case object GET extends HttpMethod
case object POST extends HttpMethod
case object PUT extends HttpMethod
case object DELETE extends HttpMethod

case class HttpRequest(method: HttpMethod, url: String, body: Option[String] = None)

implicit def stringToHttpMethod(method: String): HttpMethod = method.toUpperCase match {
  case "GET" => GET
  case "POST" => POST
  case "PUT" => PUT
  case "DELETE" => DELETE
  case _ => throw new IllegalArgumentException(s"Unknown HTTP method: $method")
}

def makeRequest(method: HttpMethod, url: String): HttpRequest = {
  HttpRequest(method, url)
}

// Implicit conversion allows string to be used as HttpMethod
val request1 = makeRequest("GET", "/api/users")
val request2 = makeRequest("POST", "/api/users")

println(s"Request 1: $request1")
println(s"Request 2: $request2")

Type Class Patterns

Building Type Class Hierarchies

// Basic type class
trait Serializable[T] {
  def serialize(value: T): String
  def deserialize(data: String): T
}

// Type class instances
implicit val intSerializable: Serializable[Int] = new Serializable[Int] {
  def serialize(value: Int): String = value.toString
  def deserialize(data: String): Int = data.toInt
}

implicit val stringSerializable: Serializable[String] = new Serializable[String] {
  def serialize(value: String): String = s"\"$value\""
  def deserialize(data: String): String = data.stripPrefix("\"").stripSuffix("\"")
}

implicit val booleanSerializable: Serializable[Boolean] = new Serializable[Boolean] {
  def serialize(value: Boolean): String = value.toString
  def deserialize(data: String): Boolean = data.toBoolean
}

// Generic serialization functions
def serialize[T](value: T)(implicit ser: Serializable[T]): String = ser.serialize(value)
def deserialize[T](data: String)(implicit ser: Serializable[T]): T = ser.deserialize(data)

println(serialize(42))
println(serialize("hello"))
println(serialize(true))

// Composition of type classes
implicit def optionSerializable[T](implicit inner: Serializable[T]): Serializable[Option[T]] = 
  new Serializable[Option[T]] {
    def serialize(value: Option[T]): String = value match {
      case Some(v) => s"Some(${inner.serialize(v)})"
      case None => "None"
    }

    def deserialize(data: String): Option[T] = {
      if (data == "None") None
      else if (data.startsWith("Some(") && data.endsWith(")")) {
        val innerData = data.substring(5, data.length - 1)
        Some(inner.deserialize(innerData))
      } else throw new IllegalArgumentException(s"Invalid Option format: $data")
    }
  }

implicit def listSerializable[T](implicit inner: Serializable[T]): Serializable[List[T]] = 
  new Serializable[List[T]] {
    def serialize(value: List[T]): String = {
      val elements = value.map(inner.serialize).mkString(",")
      s"[$elements]"
    }

    def deserialize(data: String): List[T] = {
      if (data == "[]") List.empty
      else if (data.startsWith("[") && data.endsWith("]")) {
        val content = data.substring(1, data.length - 1)
        if (content.isEmpty) List.empty
        else content.split(",").map(inner.deserialize).toList
      } else throw new IllegalArgumentException(s"Invalid List format: $data")
    }
  }

// Test composed type classes
val optionalInt: Option[Int] = Some(42)
val optionalString: Option[String] = None
val numberList = List(1, 2, 3, 4, 5)

println(serialize(optionalInt))
println(serialize(optionalString))
println(serialize(numberList))

val deserializedOpt = deserialize[Option[Int]]("Some(42)")
val deserializedList = deserialize[List[Int]]("[1,2,3,4,5]")

println(s"Deserialized option: $deserializedOpt")
println(s"Deserialized list: $deserializedList")

// Type class hierarchy
trait Semigroup[T] {
  def combine(x: T, y: T): T
}

trait Monoid[T] extends Semigroup[T] {
  def empty: T
}

trait Group[T] extends Monoid[T] {
  def inverse(x: T): T
}

// Instances for the hierarchy
implicit val intAdditionMonoid: Monoid[Int] = new Monoid[Int] {
  def combine(x: Int, y: Int): Int = x + y
  def empty: Int = 0
}

implicit val stringMonoid: Monoid[String] = new Monoid[String] {
  def combine(x: String, y: String): String = x + y
  def empty: String = ""
}

implicit def listMonoid[T]: Monoid[List[T]] = new Monoid[List[T]] {
  def combine(x: List[T], y: List[T]): List[T] = x ++ y
  def empty: List[T] = List.empty
}

// Generic operations using type classes
def combineAll[T](items: List[T])(implicit monoid: Monoid[T]): T = {
  items.foldLeft(monoid.empty)(monoid.combine)
}

def combineN[T](value: T, n: Int)(implicit semigroup: Semigroup[T]): T = {
  require(n > 0, "n must be positive")
  (1 until n).foldLeft(value)((acc, _) => semigroup.combine(acc, value))
}

println(combineAll(List(1, 2, 3, 4, 5)))
println(combineAll(List("hello", " ", "world", "!")))
println(combineAll(List(List(1, 2), List(3, 4), List(5, 6))))

println(combineN("ab", 3))
println(combineN(5, 4))

// Advanced type class: Functor hierarchy
trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

trait Applicative[F[_]] extends Functor[F] {
  def pure[A](value: A): F[A]
  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

  // Default implementation of map using ap and pure
  override def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa)
}

trait Monad[F[_]] extends Applicative[F] {
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]

  // Default implementations using flatMap
  override def map[A, B](fa: F[A])(f: A => B): F[B] = flatMap(fa)(a => pure(f(a)))
  override def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] = flatMap(ff)(f => map(fa)(f))
}

// Instances for Option
implicit val optionMonad: Monad[Option] = new Monad[Option] {
  def pure[A](value: A): Option[A] = Some(value)
  def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f)
}

// Instances for List
implicit val listMonad: Monad[List] = new Monad[List] {
  def pure[A](value: A): List[A] = List(value)
  def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.flatMap(f)
}

// Generic operations using the hierarchy
def traverse[F[_], A, B](items: List[A])(f: A => F[B])(implicit applicative: Applicative[F]): F[List[B]] = {
  items.foldRight(applicative.pure(List.empty[B])) { (item, acc) =>
    applicative.ap(applicative.map(f(item))(b => (bs: List[B]) => b :: bs))(acc)
  }
}

def sequence[F[_], A](items: List[F[A]])(implicit applicative: Applicative[F]): F[List[A]] = {
  traverse(items)(identity)
}

// Test the hierarchy
val optionList = List(Some(1), Some(2), Some(3))
val sequencedOptions = sequence(optionList)
println(s"Sequenced options: $sequencedOptions")

val listOfLists = List(List(1, 2), List(3, 4), List(5, 6))
val sequencedLists = sequence(listOfLists)
println(s"Sequenced lists: $sequencedLists")

// Type class with laws
trait Eq[T] {
  def eqv(x: T, y: T): Boolean
  def neqv(x: T, y: T): Boolean = !eqv(x, y)

  // Laws (should hold for all instances):
  // 1. Reflexivity: eqv(x, x) == true
  // 2. Symmetry: eqv(x, y) == eqv(y, x)
  // 3. Transitivity: if eqv(x, y) && eqv(y, z) then eqv(x, z)
}

implicit val intEq: Eq[Int] = new Eq[Int] {
  def eqv(x: Int, y: Int): Boolean = x == y
}

implicit val stringEq: Eq[String] = new Eq[String] {
  def eqv(x: String, y: String): Boolean = x == y
}

implicit def optionEq[T](implicit inner: Eq[T]): Eq[Option[T]] = new Eq[Option[T]] {
  def eqv(x: Option[T], y: Option[T]): Boolean = (x, y) match {
    case (Some(a), Some(b)) => inner.eqv(a, b)
    case (None, None) => true
    case _ => false
  }
}

def areEqual[T](x: T, y: T)(implicit eq: Eq[T]): Boolean = eq.eqv(x, y)

println(areEqual(42, 42))
println(areEqual("hello", "hello"))
println(areEqual(Some(10), Some(10)))
println(areEqual(Some(10), Some(20)))
println(areEqual(None: Option[Int], None: Option[Int]))

Advanced Type Class Techniques

// Type class with multiple type parameters
trait Converter[From, To] {
  def convert(value: From): To
}

// Bidirectional conversion
trait BiConverter[A, B] extends Converter[A, B] with Converter[B, A] {
  def convertTo(value: A): B = convert(value)
  def convertFrom(value: B): A = convert(value)
}

implicit val stringIntBiConverter: BiConverter[String, Int] = new BiConverter[String, Int] {
  def convert(value: String): Int = value.toInt
  def convert(value: Int): String = value.toString
}

def convertBoth[A, B](a: A, b: B)(implicit converter: BiConverter[A, B]): (B, A) = {
  (converter.convert(a), converter.convert(b))
}

val (intFromString, stringFromInt) = convertBoth("42", 100)
println(s"Converted: $intFromString, $stringFromInt")

// Type class with constraints
trait Numeric[T] {
  def plus(x: T, y: T): T
  def times(x: T, y: T): T
  def zero: T
  def one: T
}

implicit val intNumeric: Numeric[Int] = new Numeric[Int] {
  def plus(x: Int, y: Int): Int = x + y
  def times(x: Int, y: Int): Int = x * y
  def zero: Int = 0
  def one: Int = 1
}

implicit val doubleNumeric: Numeric[Double] = new Numeric[Double] {
  def plus(x: Double, y: Double): Double = x + y
  def times(x: Double, y: Double): Double = x * y
  def zero: Double = 0.0
  def one: Double = 1.0
}

// Generic mathematical operations
def power[T](base: T, exponent: Int)(implicit numeric: Numeric[T]): T = {
  require(exponent >= 0, "Exponent must be non-negative")

  def loop(acc: T, remaining: Int): T = {
    if (remaining == 0) acc
    else loop(numeric.times(acc, base), remaining - 1)
  }

  if (exponent == 0) numeric.one
  else loop(base, exponent - 1)
}

def sum[T](values: List[T])(implicit numeric: Numeric[T]): T = {
  values.foldLeft(numeric.zero)(numeric.plus)
}

println(power(2, 8))
println(power(2.5, 3))
println(sum(List(1, 2, 3, 4, 5)))
println(sum(List(1.1, 2.2, 3.3, 4.4, 5.5)))

// Type class derivation
trait Derivable[T] {
  def derive: T
}

// Automatic derivation for case classes (simplified)
import scala.reflect.ClassTag

implicit def deriveForCaseClass[T <: Product](implicit ct: ClassTag[T]): Derivable[T] = 
  new Derivable[T] {
    def derive: T = {
      // This is a simplified example - real derivation would be more complex
      throw new UnsupportedOperationException("Derivation not implemented for this example")
    }
  }

// Type class with phantom types
trait Tagged[T]
type UserId = Int with Tagged[User]
type ProductId = Int with Tagged[Product]

case class User(id: UserId, name: String)
case class Product(id: ProductId, name: String, price: BigDecimal)

trait Repository[Id, Entity] {
  def findById(id: Id): Option[Entity]
  def save(entity: Entity): Entity
}

class UserRepository extends Repository[UserId, User] {
  private var users = Map.empty[UserId, User]

  def findById(id: UserId): Option[User] = users.get(id)
  def save(user: User): User = {
    users += user.id -> user
    user
  }
}

class ProductRepository extends Repository[ProductId, Product] {
  private var products = Map.empty[ProductId, Product]

  def findById(id: ProductId): Option[Product] = products.get(id)
  def save(product: Product): Product = {
    products += product.id -> product
    product
  }
}

// Type class for validation
trait Validator[T] {
  def validate(value: T): List[String]  // List of error messages
}

implicit val emailValidator: Validator[String] = new Validator[String] {
  def validate(email: String): List[String] = {
    val errors = scala.collection.mutable.ListBuffer[String]()

    if (email.isEmpty) errors += "Email cannot be empty"
    if (!email.contains("@")) errors += "Email must contain @"
    if (email.length > 254) errors += "Email too long"

    errors.toList
  }
}

case class Age(value: Int)

implicit val ageValidator: Validator[Age] = new Validator[Age] {
  def validate(age: Age): List[String] = {
    val errors = scala.collection.mutable.ListBuffer[String]()

    if (age.value < 0) errors += "Age cannot be negative"
    if (age.value > 150) errors += "Age seems unrealistic"

    errors.toList
  }
}

// Combine validators
implicit def tupleValidator[A, B](implicit va: Validator[A], vb: Validator[B]): Validator[(A, B)] = 
  new Validator[(A, B)] {
    def validate(value: (A, B)): List[String] = 
      va.validate(value._1) ++ vb.validate(value._2)
  }

def validateAndReport[T](value: T)(implicit validator: Validator[T]): Unit = {
  val errors = validator.validate(value)
  if (errors.isEmpty) {
    println(s"✓ Valid: $value")
  } else {
    println(s"✗ Invalid: $value")
    errors.foreach(error => println(s"  - $error"))
  }
}

validateAndReport("test@example.com")
validateAndReport("invalid-email")
validateAndReport(Age(25))
validateAndReport(Age(-5))
validateAndReport(("test@example.com", Age(30)))
validateAndReport(("bad-email", Age(200)))

// Type class composition patterns
trait Codec[T] extends Serializable[T] with Validator[T] {
  def encode(value: T): Either[List[String], String] = {
    val errors = validate(value)
    if (errors.isEmpty) Right(serialize(value))
    else Left(errors)
  }

  def decode(data: String): Either[String, T] = {
    try {
      Right(deserialize(data))
    } catch {
      case e: Exception => Left(e.getMessage)
    }
  }
}

implicit val safeStringCodec: Codec[String] = new Codec[String] {
  def serialize(value: String): String = s"\"$value\""
  def deserialize(data: String): String = data.stripPrefix("\"").stripSuffix("\"")
  def validate(value: String): List[String] = 
    if (value.length > 100) List("String too long") else List.empty
}

def safeEncodeDecode[T](value: T)(implicit codec: Codec[T]): Unit = {
  codec.encode(value) match {
    case Right(encoded) => 
      println(s"Encoded: $encoded")
      codec.decode(encoded) match {
        case Right(decoded) => println(s"Decoded: $decoded")
        case Left(error) => println(s"Decode error: $error")
      }
    case Left(errors) => 
      println(s"Validation errors: ${errors.mkString(", ")}")
  }
}

safeEncodeDecode("Hello, World!")
safeEncodeDecode("x" * 101)  // Should fail validation

Building DSLs with Implicits

Domain-Specific Language Design

// SQL-like DSL
sealed trait SqlExpression
case class Column(name: String) extends SqlExpression
case class Value(data: Any) extends SqlExpression
case class BinaryOp(left: SqlExpression, op: String, right: SqlExpression) extends SqlExpression

case class SelectQuery(
  columns: List[String],
  table: String,
  where: Option[SqlExpression] = None,
  orderBy: Option[String] = None,
  limit: Option[Int] = None
)

// DSL builders
implicit class StringOps(column: String) {
  def ===(value: Any): SqlExpression = BinaryOp(Column(column), "=", Value(value))
  def !==(value: Any): SqlExpression = BinaryOp(Column(column), "!=", Value(value))
  def >(value: Any): SqlExpression = BinaryOp(Column(column), ">", Value(value))
  def <(value: Any): SqlExpression = BinaryOp(Column(column), "<", Value(value))
  def like(value: String): SqlExpression = BinaryOp(Column(column), "LIKE", Value(value))
}

implicit class SqlExpressionOps(left: SqlExpression) {
  def and(right: SqlExpression): SqlExpression = BinaryOp(left, "AND", right)
  def or(right: SqlExpression): SqlExpression = BinaryOp(left, "OR", right)
}

object SQL {
  def select(columns: String*): SelectBuilder = new SelectBuilder(columns.toList)

  class SelectBuilder(columns: List[String]) {
    def from(table: String): FromBuilder = new FromBuilder(columns, table)
  }

  class FromBuilder(columns: List[String], table: String) {
    def where(condition: SqlExpression): WhereBuilder = 
      new WhereBuilder(SelectQuery(columns, table, Some(condition)))

    def orderBy(column: String): OrderByBuilder = 
      new OrderByBuilder(SelectQuery(columns, table), column)

    def limit(n: Int): SelectQuery = 
      SelectQuery(columns, table, None, None, Some(n))

    def build: SelectQuery = SelectQuery(columns, table)
  }

  class WhereBuilder(query: SelectQuery) {
    def orderBy(column: String): OrderByBuilder = 
      new OrderByBuilder(query, column)

    def limit(n: Int): SelectQuery = 
      query.copy(limit = Some(n))

    def build: SelectQuery = query
  }

  class OrderByBuilder(query: SelectQuery, orderColumn: String) {
    def limit(n: Int): SelectQuery = 
      query.copy(orderBy = Some(orderColumn), limit = Some(n))

    def build: SelectQuery = 
      query.copy(orderBy = Some(orderColumn))
  }
}

// SQL rendering
def renderSql(query: SelectQuery): String = {
  val columnsStr = query.columns.mkString(", ")
  val baseQuery = s"SELECT $columnsStr FROM ${query.table}"

  val withWhere = query.where match {
    case Some(condition) => s"$baseQuery WHERE ${renderExpression(condition)}"
    case None => baseQuery
  }

  val withOrderBy = query.orderBy match {
    case Some(column) => s"$withWhere ORDER BY $column"
    case None => withWhere
  }

  val withLimit = query.limit match {
    case Some(n) => s"$withOrderBy LIMIT $n"
    case None => withOrderBy
  }

  withLimit
}

def renderExpression(expr: SqlExpression): String = expr match {
  case Column(name) => name
  case Value(data) => data match {
    case s: String => s"'$s'"
    case other => other.toString
  }
  case BinaryOp(left, op, right) => s"${renderExpression(left)} $op ${renderExpression(right)}"
}

// DSL usage
val query1 = SQL.select("name", "age")
  .from("users")
  .where("age" > 18 and "status" === "active")
  .orderBy("name")
  .limit(10)

val query2 = SQL.select("*")
  .from("products")
  .where("price" > 100 and ("category" === "electronics" or "category" === "gadgets"))
  .limit(5)

println("Query 1:")
println(renderSql(query1))
println("\nQuery 2:")
println(renderSql(query2))

// Configuration DSL
trait ConfigValue
case class StringValue(value: String) extends ConfigValue
case class IntValue(value: Int) extends ConfigValue
case class BooleanValue(value: Boolean) extends ConfigValue
case class ListValue(values: List[ConfigValue]) extends ConfigValue

case class Configuration(values: Map[String, ConfigValue]) {
  def getString(key: String): Option[String] = values.get(key).collect {
    case StringValue(v) => v
  }

  def getInt(key: String): Option[Int] = values.get(key).collect {
    case IntValue(v) => v
  }

  def getBoolean(key: String): Option[Boolean] = values.get(key).collect {
    case BooleanValue(v) => v
  }
}

object Config {
  class ConfigBuilder {
    private var config = Map.empty[String, ConfigValue]

    def set(key: String, value: String): ConfigBuilder = {
      config += key -> StringValue(value)
      this
    }

    def set(key: String, value: Int): ConfigBuilder = {
      config += key -> IntValue(value)
      this
    }

    def set(key: String, value: Boolean): ConfigBuilder = {
      config += key -> BooleanValue(value)
      this
    }

    def set(key: String, values: List[ConfigValue]): ConfigBuilder = {
      config += key -> ListValue(values)
      this
    }

    def build: Configuration = Configuration(config)
  }

  def builder: ConfigBuilder = new ConfigBuilder

  // DSL methods
  def apply(pairs: (String, ConfigValue)*): Configuration = {
    Configuration(pairs.toMap)
  }
}

// Implicit conversions for config DSL
implicit def stringToConfigValue(s: String): ConfigValue = StringValue(s)
implicit def intToConfigValue(i: Int): ConfigValue = IntValue(i)
implicit def booleanToConfigValue(b: Boolean): ConfigValue = BooleanValue(b)

// Configuration DSL usage
val config = Config.builder
  .set("database.host", "localhost")
  .set("database.port", 5432)
  .set("database.ssl", true)
  .set("app.name", "MyApplication")
  .set("app.debug", false)
  .build

println(s"Database host: ${config.getString("database.host")}")
println(s"Database port: ${config.getInt("database.port")}")
println(s"SSL enabled: ${config.getBoolean("database.ssl")}")

// HTTP Client DSL
case class HttpRequest(
  method: String,
  url: String,
  headers: Map[String, String] = Map.empty,
  body: Option[String] = None
)

object Http {
  class RequestBuilder(method: String, url: String) {
    private var headers = Map.empty[String, String]
    private var body: Option[String] = None

    def header(key: String, value: String): RequestBuilder = {
      headers += key -> value
      this
    }

    def headers(pairs: (String, String)*): RequestBuilder = {
      headers ++= pairs
      this
    }

    def body(content: String): RequestBuilder = {
      body = Some(content)
      this
    }

    def build: HttpRequest = HttpRequest(method, url, headers, body)

    // Simulate sending the request
    def send(): String = {
      val request = build
      s"${request.method} ${request.url}\n" +
        request.headers.map { case (k, v) => s"$k: $v" }.mkString("\n") +
        request.body.map(b => s"\n\n$b").getOrElse("")
    }
  }

  def get(url: String): RequestBuilder = new RequestBuilder("GET", url)
  def post(url: String): RequestBuilder = new RequestBuilder("POST", url)
  def put(url: String): RequestBuilder = new RequestBuilder("PUT", url)
  def delete(url: String): RequestBuilder = new RequestBuilder("DELETE", url)
}

// HTTP DSL usage
val getRequest = Http.get("https://api.example.com/users")
  .header("Authorization", "Bearer token123")
  .header("Accept", "application/json")
  .send()

val postRequest = Http.post("https://api.example.com/users")
  .headers(
    "Content-Type" -> "application/json",
    "Authorization" -> "Bearer token123"
  )
  .body("""{"name": "John Doe", "email": "john@example.com"}""")
  .send()

println("GET Request:")
println(getRequest)
println("\nPOST Request:")
println(postRequest)

// Validation DSL
sealed trait ValidationResult[+T]
case class Valid[T](value: T) extends ValidationResult[T]
case class Invalid(errors: List[String]) extends ValidationResult[Nothing]

object Validation {
  def valid[T](value: T): ValidationResult[T] = Valid(value)
  def invalid(error: String): ValidationResult[Nothing] = Invalid(List(error))
  def invalid(errors: List[String]): ValidationResult[Nothing] = Invalid(errors)

  implicit class ValidationOps[T](validation: ValidationResult[T]) {
    def map[U](f: T => U): ValidationResult[U] = validation match {
      case Valid(value) => Valid(f(value))
      case Invalid(errors) => Invalid(errors)
    }

    def flatMap[U](f: T => ValidationResult[U]): ValidationResult[U] = validation match {
      case Valid(value) => f(value)
      case Invalid(errors) => Invalid(errors)
    }

    def combine[U, V](other: ValidationResult[U])(f: (T, U) => V): ValidationResult[V] = {
      (validation, other) match {
        case (Valid(v1), Valid(v2)) => Valid(f(v1, v2))
        case (Invalid(e1), Invalid(e2)) => Invalid(e1 ++ e2)
        case (Invalid(e), _) => Invalid(e)
        case (_, Invalid(e)) => Invalid(e)
      }
    }
  }

  // Validation rules
  def notEmpty(field: String)(value: String): ValidationResult[String] = {
    if (value.trim.nonEmpty) valid(value) else invalid(s"$field cannot be empty")
  }

  def minLength(field: String, min: Int)(value: String): ValidationResult[String] = {
    if (value.length >= min) valid(value) else invalid(s"$field must be at least $min characters")
  }

  def isEmail(field: String)(value: String): ValidationResult[String] = {
    if (value.contains("@") && value.contains(".")) valid(value) 
    else invalid(s"$field must be a valid email")
  }

  def isPositive(field: String)(value: Int): ValidationResult[Int] = {
    if (value > 0) valid(value) else invalid(s"$field must be positive")
  }
}

// Validation DSL usage
import Validation._

case class UserRegistration(name: String, email: String, age: Int)

def validateUserRegistration(name: String, email: String, age: Int): ValidationResult[UserRegistration] = {
  val nameValidation = notEmpty("Name")(name).flatMap(minLength("Name", 2))
  val emailValidation = notEmpty("Email")(email).flatMap(isEmail("Email"))
  val ageValidation = isPositive("Age")(age)

  nameValidation.combine(emailValidation)((n, e) => (n, e))
    .combine(ageValidation)((ne, a) => UserRegistration(ne._1, ne._2, a))
}

// Test validation
val validUser = validateUserRegistration("John Doe", "john@example.com", 25)
val invalidUser = validateUserRegistration("", "invalid-email", -5)

validUser match {
  case Valid(user) => println(s"✓ Valid user: $user")
  case Invalid(errors) => println(s"✗ Validation errors: ${errors.mkString(", ")}")
}

invalidUser match {
  case Valid(user) => println(s"✓ Valid user: $user")
  case Invalid(errors) => 
    println("✗ Validation errors:")
    errors.foreach(error => println(s"  - $error"))
}

Summary

In this lesson, you've mastered Scala's implicit system and type classes:

Implicit Parameters: Context-dependent behavior and type class instances
Implicit Conversions: Enhanced APIs and DSL syntax
Type Class Patterns: Flexible, composable abstractions
Type Class Hierarchies: Building sophisticated abstraction layers
DSL Design: Creating domain-specific languages with natural syntax
Advanced Techniques: Validation, serialization, and generic programming
Best Practices: Maintainable and discoverable implicit usage

The implicit system enables powerful abstractions and elegant APIs while maintaining type safety and compile-time guarantees, making it a cornerstone of idiomatic Scala programming.

What's Next

In the next lesson, we'll explore metaprogramming and macros, learning how to generate code at compile time and create powerful libraries that provide zero-cost abstractions and advanced developer productivity tools.