Why Macros?

Macros let you run Scala code at compile time — inspecting and generating AST nodes before the compiler produces bytecode. The use cases are compelling:

  • Derive type class instances without runtime reflection
  • Validate string literals (regex, SQL, URLs) at compile time
  • Generate boilerplate: codecs, lenses, printers
  • Build DSLs with precise error messages

Scala 3 introduced a completely redesigned macro system that is safer, more principled, and easier to debug than the Scala 2 equivalent.

Inline and Transparent

The simplest form of compile-time computation is inline:

inline def log(inline msg: String): Unit =
  println(s"[${compiletime.codeOf(msg)}] $msg")

inline functions are expanded at call sites — no function call overhead, and the body can inspect the static shape of its arguments.

transparent inline goes further: the return type is refined to the actual computed type at each call site:

transparent inline def choose(b: Boolean): Any =
  inline if b then 42 else "hello"

val x = choose(true)   // x: Int = 42
val y = choose(false)  // y: String = "hello"

Compile-Time Assertions

You can reject invalid calls with a precise compiler error:

inline def assertPositive(inline n: Int): Int =
  inline if n > 0 then n
  else compiletime.error("n must be positive at compile time")

val ok  = assertPositive(5)   // compiles
val bad = assertPositive(-1)  // error: n must be positive at compile time

Zero runtime cost — the check happens entirely during compilation.

Quotes and Splices

For full macro power, Scala 3 provides quotes ('{ ... }) and splices (${ ... }):

import scala.quoted.*

inline def debug[A](inline a: A): A = ${ debugImpl('a) }

def debugImpl[A: Type](a: Expr[A])(using Quotes): Expr[A] =
  '{
    val result = $a
    println(s"${${ Expr(a.show) }} = $result")
    result
  }

Deriving Type Classes

The most practical use of macros in day-to-day Scala is type class derivation:

case class User(name: String, age: Int) derives JsonCodec

The derives clause invokes a macro that inspects User's fields at compile time and generates the codec — no reflection, no runtime overhead.