Metaprogramming and Macros: Code Generation and Compile-Time Magic

Introduction

Metaprogramming in Scala allows you to write programs that write programs. Through macros, reflection, and compile-time programming, you can create powerful abstractions that generate optimized code, provide zero-cost abstractions, and build sophisticated developer tools.

This lesson will teach you to leverage Scala's metaprogramming features to create libraries and tools that feel magical to use while maintaining type safety and performance. You'll learn to write macros, work with the abstract syntax tree (AST), and build compile-time utilities.

Understanding Scala's Metaprogramming Landscape

Overview of Metaprogramming Features

// Runtime reflection basics
import scala.reflect.runtime.universe._

def inspectType[T: TypeTag](value: T): Unit = {
  val tpe = typeOf[T]
  println(s"Type: $tpe")
  println(s"Type symbol: ${tpe.typeSymbol}")
  println(s"Type arguments: ${tpe.typeArgs}")

  // Get members
  val members = tpe.members.filter(!_.isMethod || _.asMethod.isAccessor)
  println(s"Members: ${members.map(_.name).mkString(", ")}")
}

case class Person(name: String, age: Int, email: String)
inspectType(Person("John", 30, "john@example.com"))
inspectType(List(1, 2, 3))
inspectType(Map("key" -> "value"))

// Type checking at runtime
def isSubtypeOf[A: TypeTag, B: TypeTag]: Boolean = {
  typeOf[A] <:< typeOf[B]
}

println(s"String <: AnyRef: ${isSubtypeOf[String, AnyRef]}")
println(s"Int <: AnyVal: ${isSubtypeOf[Int, AnyVal]}")
println(s"List[String] <: Seq[String]: ${isSubtypeOf[List[String], Seq[String]]}")

// Working with class tags
import scala.reflect.ClassTag

def createArray[T: ClassTag](elements: T*): Array[T] = {
  val array = new Array[T](elements.length)
  elements.zipWithIndex.foreach { case (elem, index) =>
    array(index) = elem
  }
  array
}

val stringArray = createArray("hello", "world", "scala")
val intArray = createArray(1, 2, 3, 4, 5)

println(s"String array: ${stringArray.mkString("[", ", ", "]")}")
println(s"Int array: ${intArray.mkString("[", ", ", "]")}")

// Type erasure and manifests
def getGenericType[T](value: T)(implicit tag: TypeTag[T]): String = {
  val tpe = tag.tpe
  tpe.toString
}

println(getGenericType(List(1, 2, 3)))
println(getGenericType(Map("a" -> 1, "b" -> 2)))
println(getGenericType(Some("hello")))

// Structural types for duck typing
type Closeable = { def close(): Unit }

def withResource[T <: Closeable, R](resource: T)(operation: T => R): R = {
  try {
    operation(resource)
  } finally {
    resource.close()
  }
}

// Mock closeable resource
class MockResource {
  private var closed = false
  def process(data: String): String = s"Processing: $data"
  def close(): Unit = {
    closed = true
    println("Resource closed")
  }
}

val result = withResource(new MockResource) { resource =>
  resource.process("important data")
}
println(s"Result: $result")

// Compile-time type computations
trait TypeLevel[T] {
  type Result
  def compute(value: T): Result
}

implicit val stringTypeLevel: TypeLevel[String] = new TypeLevel[String] {
  type Result = Int
  def compute(value: String): Int = value.length
}

implicit val intTypeLevel: TypeLevel[Int] = new TypeLevel[Int] {
  type Result = String
  def compute(value: Int): String = value.toString
}

def transform[T](value: T)(implicit tl: TypeLevel[T]): tl.Result = {
  tl.compute(value)
}

val lengthResult = transform("Hello, World!")  // Returns Int
val stringResult = transform(42)              // Returns String

println(s"String length: $lengthResult")
println(s"Int as string: $stringResult")

// Phantom types for compile-time validation
trait Validated
trait Unvalidated

case class Email[T](value: String)

object Email {
  def apply(value: String): Email[Unvalidated] = new Email[Unvalidated](value)

  def validate(email: Email[Unvalidated]): Option[Email[Validated]] = {
    if (email.value.contains("@") && email.value.contains(".")) {
      Some(new Email[Validated](email.value))
    } else {
      None
    }
  }
}

def sendEmail(email: Email[Validated]): String = {
  s"Sending email to ${email.value}"
}

val unvalidatedEmail = Email("test@example.com")
val validatedEmail = Email.validate(unvalidatedEmail)

validatedEmail match {
  case Some(email) => println(sendEmail(email))
  case None => println("Invalid email")
}

// This would not compile:
// sendEmail(unvalidatedEmail)  // Type mismatch

// Type-level computations with Shapeless-style HLists
sealed trait HList
case object HNil extends HList
case class ::[H, T <: HList](head: H, tail: T) extends HList

type StringIntBool = String :: Int :: Boolean :: HNil

val hlist: StringIntBool = "hello" :: 42 :: true :: HNil

// Type-level length computation
trait Length[L <: HList] {
  type Out <: Nat
  def apply(): Int
}

sealed trait Nat
case object Zero extends Nat
case class Succ[N <: Nat]() extends Nat

implicit val hnilLength: Length[HNil] = new Length[HNil] {
  type Out = Zero.type
  def apply(): Int = 0
}

implicit def hconsLength[H, T <: HList](implicit tl: Length[T]): Length[H :: T] = 
  new Length[H :: T] {
    type Out = Succ[tl.Out]
    def apply(): Int = 1 + tl.apply()
  }

def length[L <: HList](hlist: L)(implicit len: Length[L]): Int = len.apply()

println(s"HList length: ${length(hlist)}")

// Dependent types
trait DepType {
  type T
  def value: T
}

object DepType {
  def apply[A](v: A): DepType { type T = A } = new DepType {
    type T = A
    def value: A = v
  }
}

val depString = DepType("hello")
val depInt = DepType(42)

println(s"Dependent string: ${depString.value}")
println(s"Dependent int: ${depInt.value}")

// Type refinements
trait Base {
  type Inner
  def create: Inner
}

def useBase(base: Base { type Inner = String }): String = {
  base.create.toUpperCase
}

val stringBase = new Base {
  type Inner = String
  def create: String = "hello world"
}

println(useBase(stringBase))

Advanced Type-Level Programming

// Type-level boolean logic
sealed trait Bool
case object True extends Bool
case object False extends Bool

trait Not[B <: Bool] {
  type Out <: Bool
}

implicit val notTrue: Not[True.type] = new Not[True.type] {
  type Out = False.type
}

implicit val notFalse: Not[False.type] = new Not[False.type] {
  type Out = True.type
}

trait And[A <: Bool, B <: Bool] {
  type Out <: Bool
}

implicit val trueAndTrue: And[True.type, True.type] = new And[True.type, True.type] {
  type Out = True.type
}

implicit val trueAndFalse: And[True.type, False.type] = new And[True.type, False.type] {
  type Out = False.type
}

implicit val falseAndTrue: And[False.type, True.type] = new And[False.type, True.type] {
  type Out = False.type
}

implicit val falseAndFalse: And[False.type, False.type] = new And[False.type, False.type] {
  type Out = False.type
}

// Type-level natural numbers with operations
trait TNat
case object TZero extends TNat
case class TSucc[N <: TNat]() extends TNat

type T0 = TZero.type
type T1 = TSucc[T0]
type T2 = TSucc[T1]
type T3 = TSucc[T2]
type T4 = TSucc[T3]

trait ToInt[N <: TNat] {
  def apply(): Int
}

implicit val zeroToInt: ToInt[T0] = new ToInt[T0] {
  def apply(): Int = 0
}

implicit def succToInt[N <: TNat](implicit n: ToInt[N]): ToInt[TSucc[N]] = 
  new ToInt[TSucc[N]] {
    def apply(): Int = 1 + n.apply()
  }

trait Add[A <: TNat, B <: TNat] {
  type Out <: TNat
}

implicit def addZero[A <: TNat]: Add[A, T0] = new Add[A, T0] {
  type Out = A
}

implicit def addSucc[A <: TNat, B <: TNat](implicit add: Add[A, B]): Add[A, TSucc[B]] = 
  new Add[A, TSucc[B]] {
    type Out = TSucc[add.Out]
  }

def add[A <: TNat, B <: TNat](implicit add: Add[A, B], toInt: ToInt[add.Out]): Int = {
  toInt.apply()
}

val sum23: Int = add[T2, T3]  // 2 + 3 = 5
println(s"2 + 3 = $sum23")

// Type-level lists and operations
sealed trait TList
case object TNil extends TList
case class TCons[H, T <: TList]() extends TList

type TStringIntList = TCons[String, TCons[Int, TNil.type]]

trait Contains[L <: TList, T] {
  type Out <: Bool
}

implicit def containsNil[T]: Contains[TNil.type, T] = new Contains[TNil.type, T] {
  type Out = False.type
}

implicit def containsHead[H, T <: TList]: Contains[TCons[H, T], H] = 
  new Contains[TCons[H, T], H] {
    type Out = True.type
  }

implicit def containsTail[H, T <: TList, U](implicit contains: Contains[T, U]): Contains[TCons[H, T], U] = 
  new Contains[TCons[H, T], U] {
    type Out = contains.Out
  }

// Prove that our list contains String and Int
val containsString = implicitly[Contains[TStringIntList, String] { type Out = True.type }]
val containsInt = implicitly[Contains[TStringIntList, Int] { type Out = True.type }]

// This would not compile:
// val containsDouble = implicitly[Contains[TStringIntList, Double] { type Out = True.type }]

println("Type-level list operations successful")

// Type-level programming with evidence
trait =:=[A, B]

implicit def evidence[A]: A =:= A = new (A =:= A) {}

def cast[A, B](value: A)(implicit ev: A =:= B): B = value.asInstanceOf[B]

val stringAsString = cast[String, String]("hello")
// val stringAsInt = cast[String, Int]("hello")  // Would not compile

// Type-level computation with implicits
trait LessThan[A <: TNat, B <: TNat]

implicit def zeroLessThanSucc[N <: TNat]: LessThan[T0, TSucc[N]] = 
  new LessThan[T0, TSucc[N]] {}

implicit def succLessThanSucc[A <: TNat, B <: TNat](implicit lt: LessThan[A, B]): LessThan[TSucc[A], TSucc[B]] = 
  new LessThan[TSucc[A], TSucc[B]] {}

// Safe array access using type-level bounds
class SafeArray[T, N <: TNat](private val array: Array[T]) {
  def get[I <: TNat](index: I)(implicit 
    indexToInt: ToInt[I], 
    lt: LessThan[I, N]
  ): T = {
    array(indexToInt.apply())
  }

  def length(implicit nToInt: ToInt[N]): Int = nToInt.apply()
}

object SafeArray {
  def apply[T](elements: T*): SafeArray[T, _] = {
    // This is a simplified version - real implementation would track length at type level
    new SafeArray(elements.toArray).asInstanceOf[SafeArray[T, T4]]  // Assuming max 4 elements
  }
}

val safeArray = SafeArray("a", "b", "c")
// val element = safeArray.get[T2]  // Would work
// val outOfBounds = safeArray.get[T4]  // Would not compile due to bounds check

println("Safe array access successful")

// Singleton types for compile-time strings
trait Witness[T] {
  type T1 = T
  def value: T
}

object Witness {
  def apply[T](v: T): Witness[T] = new Witness[T] {
    def value: T = v
  }

  implicit def witness[T](implicit value: T): Witness[T] = apply(value)
}

// Compile-time string operations (simplified)
trait StrLen[S] {
  type Out <: TNat
}

// This would require macro implementation for real string length computation
implicit def strLen(implicit s: Witness[String]): StrLen[s.T1] = new StrLen[s.T1] {
  type Out = T4  // Simplified - would actually compute length
}

// Heterogeneous maps with type-level keys
trait Key[T] {
  type Value
}

case class StringKey() extends Key[String] {
  type Value = String
}

case class IntKey() extends Key[Int] {
  type Value = Int
}

class HMap private (private val map: Map[Any, Any]) {
  def get[T](key: Key[T]): Option[key.Value] = {
    map.get(key).asInstanceOf[Option[key.Value]]
  }

  def put[T](key: Key[T], value: key.Value): HMap = {
    new HMap(map + (key -> value))
  }
}

object HMap {
  def empty: HMap = new HMap(Map.empty)
}

val hmap = HMap.empty
  .put(StringKey(), "hello")
  .put(IntKey(), 42)

println(s"String value: ${hmap.get(StringKey())}")
println(s"Int value: ${hmap.get(IntKey())}")

// Type providers (compile-time resource loading)
trait TypeProvider[T] {
  def provide: T
}

// This would be implemented with macros to load resources at compile time
class ConfigProvider(path: String) extends TypeProvider[Map[String, String]] {
  def provide: Map[String, String] = {
    // Simplified - would actually read file at compile time
    Map("key1" -> "value1", "key2" -> "value2")
  }
}

implicit val configProvider: TypeProvider[Map[String, String]] = new ConfigProvider("config.properties")

def getConfig(implicit provider: TypeProvider[Map[String, String]]): Map[String, String] = {
  provider.provide
}

val config = getConfig
println(s"Config: $config")

Building Your First Macros

Scala 2 Macro Basics

// Note: This section shows Scala 2 style macros for educational purposes
// Modern Scala 3 uses inline and transparent inline

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

// Simple macro example
object DebugMacros {
  def debug(value: Any): Unit = macro debugImpl

  def debugImpl(c: blackbox.Context)(value: c.Expr[Any]): c.Expr[Unit] = {
    import c.universe._

    val valueCode = value.tree.toString
    val debugCode = q"""
      println(s"DEBUG: $valueCode = " + $value)
    """

    c.Expr[Unit](debugCode)
  }
}

// Usage (this would work in a real Scala 2 project with macro paradise)
// DebugMacros.debug(2 + 2)  // Prints: DEBUG: 2 + 2 = 4
// DebugMacros.debug("hello".toUpperCase)  // Prints: DEBUG: "hello".toUpperCase = HELLO

// Macro for compile-time assertions
object AssertMacros {
  def assert(condition: Boolean): Unit = macro assertImpl

  def assertImpl(c: blackbox.Context)(condition: c.Expr[Boolean]): c.Expr[Unit] = {
    import c.universe._

    val conditionCode = condition.tree.toString
    val assertCode = q"""
      if (!$condition) {
        throw new AssertionError(s"Assertion failed: $conditionCode")
      }
    """

    c.Expr[Unit](assertCode)
  }
}

// Macro for automatic case class lens generation
trait Lens[S, A] {
  def get(s: S): A
  def set(s: S, a: A): S
}

object LensMacros {
  def mkLens[S, A](path: S => A): Lens[S, A] = macro mkLensImpl[S, A]

  def mkLensImpl[S: c.WeakTypeTag, A: c.WeakTypeTag](c: blackbox.Context)(
    path: c.Expr[S => A]
  ): c.Expr[Lens[S, A]] = {
    import c.universe._

    // Extract field access from the path function
    val q"($_) => $field" = path.tree

    val fieldName = field match {
      case q"$_.${fieldName: TermName}" => fieldName
      case _ => c.abort(c.enclosingPosition, "Path must be a simple field access")
    }

    val sType = weakTypeOf[S]
    val aType = weakTypeOf[A]

    val lensCode = q"""
      new Lens[$sType, $aType] {
        def get(s: $sType): $aType = s.$fieldName
        def set(s: $sType, a: $aType): $sType = s.copy($fieldName = a)
      }
    """

    c.Expr[Lens[S, A]](lensCode)
  }
}

// Macro for compile-time JSON parsing
object JsonMacros {
  case class JsonObject(fields: Map[String, JsonValue])
  case class JsonArray(elements: List[JsonValue])

  sealed trait JsonValue
  case class JsonString(value: String) extends JsonValue
  case class JsonNumber(value: Double) extends JsonValue
  case class JsonBoolean(value: Boolean) extends JsonValue
  case object JsonNull extends JsonValue

  def parseJson(json: String): JsonValue = macro parseJsonImpl

  def parseJsonImpl(c: blackbox.Context)(json: c.Expr[String]): c.Expr[JsonValue] = {
    import c.universe._

    // Extract compile-time string literal
    val jsonString = json.tree match {
      case Literal(Constant(s: String)) => s
      case _ => c.abort(c.enclosingPosition, "JSON must be a string literal")
    }

    // Parse JSON at compile time (simplified parser)
    def parseValue(str: String): Tree = {
      str.trim match {
        case s if s.startsWith("\"") && s.endsWith("\"") =>
          val content = s.substring(1, s.length - 1)
          q"JsonString($content)"
        case s if s == "true" => q"JsonBoolean(true)"
        case s if s == "false" => q"JsonBoolean(false)"
        case s if s == "null" => q"JsonNull"
        case s if s.forall(c => c.isDigit || c == '.' || c == '-') =>
          val num = s.toDouble
          q"JsonNumber($num)"
        case _ => c.abort(c.enclosingPosition, s"Cannot parse JSON value: $str")
      }
    }

    val parsedTree = parseValue(jsonString)
    c.Expr[JsonValue](parsedTree)
  }
}

// Macro for automatic toString generation
object ToStringMacros {
  def autoToString[T]: String = macro autoToStringImpl[T]

  def autoToStringImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[String] = {
    import c.universe._

    val tpe = weakTypeOf[T]
    val className = tpe.typeSymbol.name.toString

    val fields = tpe.members.collect {
      case m: MethodSymbol if m.isCaseAccessor => m
    }.toList.reverse

    val fieldStrings = fields.map { field =>
      val fieldName = field.name.toString
      q"$fieldName + $$"=" + this.$field"
    }

    val toStringCode = if (fieldStrings.nonEmpty) {
      val combined = fieldStrings.reduce((a, b) => q"$a + $$", " + $b")
      q"$className + $$"(" + $combined + $$")""
    } else {
      q"$className"
    }

    c.Expr[String](toStringCode)
  }
}

// Macro for compile-time configuration validation
object ConfigMacros {
  case class Config(
    host: String,
    port: Int,
    ssl: Boolean,
    timeout: Int
  )

  def loadConfig(path: String): Config = macro loadConfigImpl

  def loadConfigImpl(c: blackbox.Context)(path: c.Expr[String]): c.Expr[Config] = {
    import c.universe._
    import scala.io.Source

    val pathString = path.tree match {
      case Literal(Constant(s: String)) => s
      case _ => c.abort(c.enclosingPosition, "Config path must be a string literal")
    }

    // Load and parse config at compile time
    try {
      val configSource = Source.fromFile(pathString)
      val configLines = configSource.getLines().toList
      configSource.close()

      val configMap = configLines.collect {
        case line if line.contains("=") =>
          val Array(key, value) = line.split("=", 2)
          key.trim -> value.trim
      }.toMap

      // Validate required keys
      val requiredKeys = Set("host", "port", "ssl", "timeout")
      val missingKeys = requiredKeys -- configMap.keySet
      if (missingKeys.nonEmpty) {
        c.abort(c.enclosingPosition, s"Missing config keys: ${missingKeys.mkString(", ")}")
      }

      // Generate config object
      val host = configMap("host")
      val port = configMap("port").toInt
      val ssl = configMap("ssl").toBoolean
      val timeout = configMap("timeout").toInt

      val configCode = q"Config($host, $port, $ssl, $timeout)"
      c.Expr[Config](configCode)

    } catch {
      case e: Exception =>
        c.abort(c.enclosingPosition, s"Failed to load config: ${e.getMessage}")
    }
  }
}

// Demonstration of macro concepts (these would work in a real macro context)
println("Macro concepts demonstrated (implementations shown for educational purposes)")

// Manual implementation of what debug macro would generate
def manualDebug[T](value: T): Unit = {
  println(s"DEBUG: value = $value")
}

manualDebug(2 + 2)
manualDebug("hello".toUpperCase)

// Manual lens implementation
case class Person(name: String, age: Int)

val nameLens = new Lens[Person, String] {
  def get(person: Person): String = person.name
  def set(person: Person, name: String): Person = person.copy(name = name)
}

val john = Person("John", 30)
val johnRenamed = nameLens.set(john, "Johnny")

println(s"Original: $john")
println(s"Renamed: $johnRenamed")

// Manual compile-time validation example
def validateAtCompileTime[T](value: T): T = {
  // In a real macro, this would happen at compile time
  require(value != null, "Value cannot be null")
  value
}

println(s"Validated: ${validateAtCompileTime("hello")}")

Code Generation Patterns

// Typeclass derivation pattern
trait Show[T] {
  def show(value: T): String
}

object Show {
  def apply[T](implicit show: Show[T]): Show[T] = show

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

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

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

  // In a real macro implementation, this would automatically derive Show for case classes
  def derived[T]: Show[T] = macro derivedImpl[T]

  // This would be the macro implementation
  def derivedImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[Show[T]] = {
    import c.universe._

    val tpe = weakTypeOf[T]
    val companion = tpe.typeSymbol.companion

    // Check if it's a case class
    if (!tpe.typeSymbol.asClass.isCaseClass) {
      c.abort(c.enclosingPosition, "Can only derive Show for case classes")
    }

    val fields = tpe.members.collect {
      case m: MethodSymbol if m.isCaseAccessor => m
    }.toList.reverse

    val className = tpe.typeSymbol.name.toString

    val showFields = fields.map { field =>
      val fieldName = field.name.toString
      val fieldType = field.returnType
      q"""$fieldName + "=" + Show[$fieldType].show(value.$field)"""
    }

    val showCode = if (showFields.nonEmpty) {
      val combined = showFields.reduce((a, b) => q"$a + $$", " + $b")
      q"""
        new Show[$tpe] {
          def show(value: $tpe): String = 
            $className + $$"(" + $combined + $$")"
        }
      """
    } else {
      q"""
        new Show[$tpe] {
          def show(value: $tpe): String = $className
        }
      """
    }

    c.Expr[Show[T]](showCode)
  }
}

// Manual case class show implementation
case class Address(street: String, city: String, zip: String)
case class Customer(name: String, age: Int, address: Address)

implicit val addressShow: Show[Address] = new Show[Address] {
  def show(value: Address): String = 
    s"Address(street=${Show[String].show(value.street)}, " +
    s"city=${Show[String].show(value.city)}, " +
    s"zip=${Show[String].show(value.zip)})"
}

implicit val customerShow: Show[Customer] = new Show[Customer] {
  def show(value: Customer): String = 
    s"Customer(name=${Show[String].show(value.name)}, " +
    s"age=${Show[Int].show(value.age)}, " +
    s"address=${Show[Address].show(value.address)})"
}

val customer = Customer("Alice", 28, Address("123 Main St", "Springfield", "12345"))
println(Show[Customer].show(customer))

// Code generation for builders
trait Builder[T] {
  def build: T
}

// This would be generated by a macro
class PersonBuilder {
  private var name: String = ""
  private var age: Int = 0
  private var email: String = ""

  def withName(name: String): PersonBuilder = {
    this.name = name
    this
  }

  def withAge(age: Int): PersonBuilder = {
    this.age = age
    this
  }

  def withEmail(email: String): PersonBuilder = {
    this.email = email
    this
  }

  def build: Person = Person(name, age)
}

object PersonBuilder {
  def apply(): PersonBuilder = new PersonBuilder()
}

val builtPerson = PersonBuilder()
  .withName("Bob")
  .withAge(35)
  .withEmail("bob@example.com")
  .build

println(s"Built person: $builtPerson")

// Code generation for lenses
case class Organization(name: String, address: Address, employees: List[Person])

// These would be generated by macros
object OrganizationLenses {
  val name: Lens[Organization, String] = new Lens[Organization, String] {
    def get(org: Organization): String = org.name
    def set(org: Organization, name: String): Organization = org.copy(name = name)
  }

  val address: Lens[Organization, Address] = new Lens[Organization, Address] {
    def get(org: Organization): Address = org.address
    def set(org: Organization, address: Address): Organization = org.copy(address = address)
  }

  val employees: Lens[Organization, List[Person]] = new Lens[Organization, List[Person]] {
    def get(org: Organization): List[Person] = org.employees
    def set(org: Organization, employees: List[Person]): Organization = org.copy(employees = employees)
  }
}

object AddressLenses {
  val street: Lens[Address, String] = new Lens[Address, String] {
    def get(addr: Address): String = addr.street
    def set(addr: Address, street: String): Address = addr.copy(street = street)
  }

  val city: Lens[Address, String] = new Lens[Address, String] {
    def get(addr: Address): String = addr.city
    def set(addr: Address, city: String): Address = addr.copy(city = city)
  }
}

// Lens composition
implicit class LensOps[S, A](lens: Lens[S, A]) {
  def compose[B](other: Lens[A, B]): Lens[S, B] = new Lens[S, B] {
    def get(s: S): B = other.get(lens.get(s))
    def set(s: S, b: B): S = lens.set(s, other.set(lens.get(s), b))
  }
}

val org = Organization(
  "Tech Corp",
  Address("456 Tech St", "San Francisco", "94105"),
  List(Person("Alice", 30), Person("Bob", 25))
)

// Composed lens for nested access
val orgStreetLens = OrganizationLenses.address.compose(AddressLenses.street)

val updatedOrg = orgStreetLens.set(org, "789 Innovation Ave")
println(s"Original street: ${orgStreetLens.get(org)}")
println(s"Updated street: ${orgStreetLens.get(updatedOrg)}")

// Code generation for serializers
trait Serializer[T] {
  def serialize(value: T): Map[String, Any]
  def deserialize(data: Map[String, Any]): T
}

// This would be generated by a macro for case classes
implicit val personSerializer: Serializer[Person] = new Serializer[Person] {
  def serialize(person: Person): Map[String, Any] = Map(
    "name" -> person.name,
    "age" -> person.age
  )

  def deserialize(data: Map[String, Any]): Person = Person(
    data("name").asInstanceOf[String],
    data("age").asInstanceOf[Int]
  )
}

implicit val addressSerializer: Serializer[Address] = new Serializer[Address] {
  def serialize(address: Address): Map[String, Any] = Map(
    "street" -> address.street,
    "city" -> address.city,
    "zip" -> address.zip
  )

  def deserialize(data: Map[String, Any]): Address = Address(
    data("street").asInstanceOf[String],
    data("city").asInstanceOf[String],
    data("zip").asInstanceOf[String]
  )
}

def serialize[T](value: T)(implicit serializer: Serializer[T]): Map[String, Any] = {
  serializer.serialize(value)
}

def deserialize[T](data: Map[String, Any])(implicit serializer: Serializer[T]): T = {
  serializer.deserialize(data)
}

val serializedPerson = serialize(builtPerson)
val deserializedPerson = deserialize[Person](serializedPerson)

println(s"Serialized: $serializedPerson")
println(s"Deserialized: $deserializedPerson")

// Phantom type generation for type-safe APIs
trait Status
object Status {
  trait Draft extends Status
  trait Published extends Status
  trait Archived extends Status
}

case class Document[S <: Status](title: String, content: String)

object Document {
  def create(title: String, content: String): Document[Status.Draft] = 
    new Document[Status.Draft](title, content)

  def publish[S <: Status](doc: Document[S]): Document[Status.Published] = 
    new Document[Status.Published](doc.title, doc.content)

  def archive[S <: Status](doc: Document[S]): Document[Status.Archived] = 
    new Document[Status.Archived](doc.title, doc.content)
}

// Only published documents can be shared
def shareDocument(doc: Document[Status.Published]): String = {
  s"Sharing: ${doc.title}"
}

val draft = Document.create("My Article", "Content here...")
val published = Document.publish(draft)
val shared = shareDocument(published)

println(shared)

// This would not compile:
// shareDocument(draft)  // Type error: Draft is not Published

println("Code generation patterns demonstrated")

Compile-Time Utilities

Compile-Time Validation and Optimization

// Compile-time string validation
trait ValidatedString[T <: String] {
  def value: T
}

object ValidatedString {
  // In a macro, this would validate the string at compile time
  def apply[T <: String](s: T): ValidatedString[T] = new ValidatedString[T] {
    def value: T = s
  }

  // Compile-time email validation (would be implemented as macro)
  def email[T <: String](s: T): ValidatedString[T] = {
    // In real implementation, this would validate email format at compile time
    require(s.contains("@"), "Must be valid email")
    apply(s)
  }

  // Compile-time URL validation
  def url[T <: String](s: T): ValidatedString[T] = {
    require(s.startsWith("http://") || s.startsWith("https://"), "Must be valid URL")
    apply(s)
  }
}

// Type-safe configuration at compile time
trait ConfigKey[T] {
  def key: String
  def default: Option[T]
}

case class StringConfig(key: String, default: Option[String] = None) extends ConfigKey[String]
case class IntConfig(key: String, default: Option[Int] = None) extends ConfigKey[Int]
case class BooleanConfig(key: String, default: Option[Boolean] = None) extends ConfigKey[Boolean]

object ConfigKeys {
  val DatabaseHost = StringConfig("database.host", Some("localhost"))
  val DatabasePort = IntConfig("database.port", Some(5432))
  val DatabaseSSL = BooleanConfig("database.ssl", Some(false))
  val AppName = StringConfig("app.name")
  val Debug = BooleanConfig("debug", Some(false))
}

// Compile-time configuration reader (would be macro-generated)
class TypedConfig private (values: Map[String, Any]) {
  def get[T](config: ConfigKey[T]): T = {
    values.get(config.key) match {
      case Some(value) => value.asInstanceOf[T]
      case None => config.default.getOrElse(
        throw new RuntimeException(s"Missing config key: ${config.key}")
      )
    }
  }
}

object TypedConfig {
  // This would be a macro that reads config at compile time
  def load(source: String): TypedConfig = {
    // Simplified - would actually parse config file at compile time
    val values = Map[String, Any](
      "database.host" -> "prod-db.example.com",
      "database.port" -> 5432,
      "database.ssl" -> true,
      "app.name" -> "MyApp"
    )
    new TypedConfig(values)
  }
}

val config = TypedConfig.load("application.conf")
println(s"Database: ${config.get(ConfigKeys.DatabaseHost)}:${config.get(ConfigKeys.DatabasePort)}")
println(s"SSL: ${config.get(ConfigKeys.DatabaseSSL)}")
println(s"App: ${config.get(ConfigKeys.AppName)}")
println(s"Debug: ${config.get(ConfigKeys.Debug)}")

// Compile-time SQL query validation
sealed trait SqlType
case object StringType extends SqlType
case object IntType extends SqlType
case object BooleanType extends SqlType

case class Column(name: String, sqlType: SqlType)
case class Table(name: String, columns: List[Column])

object Schema {
  val Users = Table("users", List(
    Column("id", IntType),
    Column("name", StringType),
    Column("email", StringType),
    Column("active", BooleanType)
  ))

  val Products = Table("products", List(
    Column("id", IntType),
    Column("name", StringType),
    Column("price", IntType),
    Column("available", BooleanType)
  ))
}

// Compile-time query builder with validation
trait Query[T] {
  def sql: String
  def extract(row: Map[String, Any]): T
}

class QueryBuilder[T](table: Table, selector: List[Column] => T) {
  def where(condition: String): Query[T] = new Query[T] {
    def sql: String = s"SELECT * FROM ${table.name} WHERE $condition"
    def extract(row: Map[String, Any]): T = selector(table.columns)
  }

  def all: Query[T] = new Query[T] {
    def sql: String = s"SELECT * FROM ${table.name}"
    def extract(row: Map[String, Any]): T = selector(table.columns)
  }
}

// Type-safe query construction
def select[T](table: Table)(selector: List[Column] => T): QueryBuilder[T] = {
  new QueryBuilder(table, selector)
}

// Extract user data safely
case class User(id: Int, name: String, email: String, active: Boolean)

val userQuery = select(Schema.Users) { columns =>
  // In a real macro implementation, this would validate column access at compile time
  User(0, "", "", true)  // Simplified
}.where("active = true")

println(s"User query: ${userQuery.sql}")

// Compile-time format string validation
trait SafeFormat[Args] {
  def format(args: Args): String
}

// This would be implemented with macros to validate format strings at compile time
object SafeFormat {
  def apply(formatString: String): SafeFormatBuilder = new SafeFormatBuilder(formatString)

  class SafeFormatBuilder(formatString: String) {
    def validate[Args]: SafeFormat[Args] = new SafeFormat[Args] {
      def format(args: Args): String = {
        // In real implementation, would validate format string against arg types
        formatString  // Simplified
      }
    }
  }
}

// Compile-time arithmetic verification
trait Verified[T] {
  def value: T
}

object Verified {
  // These would be macros that verify operations at compile time
  def positive[T: Numeric](value: T): Verified[T] = {
    val num = implicitly[Numeric[T]]
    require(num.compare(value, num.zero) > 0, "Value must be positive")
    new Verified[T] {
      def value: T = value
    }
  }

  def nonZero[T: Numeric](value: T): Verified[T] = {
    val num = implicitly[Numeric[T]]
    require(num.compare(value, num.zero) != 0, "Value must be non-zero")
    new Verified[T] {
      def value: T = value
    }
  }

  def inRange[T: Numeric](value: T, min: T, max: T): Verified[T] = {
    val num = implicitly[Numeric[T]]
    require(
      num.compare(value, min) >= 0 && num.compare(value, max) <= 0,
      s"Value must be between $min and $max"
    )
    new Verified[T] {
      def value: T = value
    }
  }
}

// Safe division with compile-time verification
def safeDivide[T: Numeric](a: T, b: Verified[T]): T = {
  val num = implicitly[Numeric[T]]
  // We know b is non-zero due to compile-time verification
  num.quot(a, b.value)
}

val positiveNumber = Verified.positive(42)
val nonZeroNumber = Verified.nonZero(5)
val rangeNumber = Verified.inRange(10, 1, 20)

println(s"Positive: ${positiveNumber.value}")
println(s"Non-zero: ${nonZeroNumber.value}")
println(s"In range: ${rangeNumber.value}")
println(s"Safe division: ${safeDivide(100, nonZeroNumber)}")

// Compile-time dependency injection validation
trait Injectable[T] {
  def provide: T
}

trait Module {
  def configure(): Unit
}

// This would be validated at compile time to ensure all dependencies are satisfied
class DatabaseModule extends Module {
  implicit val dbConnection: Injectable[String] = new Injectable[String] {
    def provide: String = "database-connection"
  }

  def configure(): Unit = println("Database module configured")
}

class ServiceModule extends Module {
  def userService(implicit db: Injectable[String]): Injectable[String] = new Injectable[String] {
    def provide: String = s"user-service-with-${db.provide}"
  }

  def configure(): Unit = println("Service module configured")
}

// Compile-time validation that all dependencies are satisfied
def wireApplication(modules: Module*): Map[String, String] = {
  modules.foreach(_.configure())

  // In real implementation, this would be validated at compile time
  Map(
    "database" -> "connected",
    "service" -> "ready"
  )
}

val app = wireApplication(new DatabaseModule(), new ServiceModule())
println(s"Application wired: $app")

// Zero-cost abstractions through compile-time generation
trait ZeroCost[T] {
  def operation(value: T): T
}

// This would generate optimized code at compile time
implicit val intZeroCost: ZeroCost[Int] = new ZeroCost[Int] {
  def operation(value: Int): T = value * 2  // Inlined at compile time
}

implicit val stringZeroCost: ZeroCost[String] = new ZeroCost[String] {
  def operation(value: String): String = value.toUpperCase  // Inlined at compile time
}

def performOperation[T](value: T)(implicit zc: ZeroCost[T]): T = {
  // This would be completely inlined, no runtime overhead
  zc.operation(value)
}

println(s"Int operation: ${performOperation(21)}")
println(s"String operation: ${performOperation("hello")}")

println("Compile-time utilities demonstrated")

Summary

In this lesson, you've mastered Scala's metaprogramming capabilities:

Runtime Reflection: Working with types, members, and class information at runtime
Type-Level Programming: Compile-time computations and type-level data structures
Macro Fundamentals: Understanding AST manipulation and code generation
Code Generation Patterns: Automatic derivation of boilerplate code
Compile-Time Validation: Static verification and optimization
Zero-Cost Abstractions: Performance through compile-time specialization
Advanced Techniques: Type providers, phantom types, and dependent types

Metaprogramming enables you to create powerful libraries and tools that feel magical while maintaining performance and type safety through compile-time code generation and validation.

What's Next

In the next lesson, we'll explore the Scala ecosystem and popular libraries, learning how to leverage the rich collection of tools and frameworks available to build real-world applications efficiently.