Metaprogramming in Scala 3: Macros
Scala 3 introduces a completely redesigned macro system that provides powerful metaprogramming capabilities while maintaining type safety and ease of use. This lesson explores the new macro API, showing you how to generate code at compile time, create domain-specific languages, and perform compile-time computations.
Introduction to Scala 3 Macros
Macros in Scala 3 allow you to generate and transform code at compile time. Unlike runtime reflection, macros provide type-safe code generation with full compiler support and IDE integration.
Key Concepts
// Core macro concepts in Scala 3:
// 1. Quotes ('{ ... }) - wrap expressions to create AST nodes
// 2. Splices (${ ... }) - insert expressions into quotes
// 3. Expr[T] - typed expression representing code of type T
// 4. Quotes - context providing compiler information
// 5. Type representations - work with types at compile time
Basic Macro Syntax
Simple Expression Macros
import scala.quoted.*
// Simple macro that prints debug information
inline def debug[T](inline value: T): T = ${ debugImpl('value) }
def debugImpl[T: Type](value: Expr[T])(using Quotes): Expr[T] = {
import quotes.reflect.*
// Extract the source code representation
val valueRepr = value.asTerm.show
val typeRepr = TypeRepr.of[T].show
// Generate code that prints debug info and returns the value
'{
println(s"Debug: ${${Expr(valueRepr)}} : ${${Expr(typeRepr)}} = ${$value}")
$value
}
}
// Usage
val result = debug(42 + 8)
// Output: Debug: scala.Int.+(42)(8) : scala.Int = 50
val name = debug("Scala")
// Output: Debug: "Scala" : java.lang.String = Scala
Compile-Time Assertions
import scala.quoted.*
// Macro for compile-time assertions
inline def staticAssert(inline condition: Boolean, inline message: String): Unit =
${ staticAssertImpl('condition, 'message) }
def staticAssertImpl(condition: Expr[Boolean], message: Expr[String])(using Quotes): Expr[Unit] = {
import quotes.reflect.*
condition.value match {
case Some(true) =>
'{ () } // Assertion passes, generate empty code
case Some(false) =>
val msg = message.valueOrAbort
report.errorAndAbort(s"Static assertion failed: $msg")
case None =>
report.errorAndAbort("Condition must be a compile-time constant")
}
}
// Usage
staticAssert(1 + 1 == 2, "Math is working") // Compiles fine
// staticAssert(1 + 1 == 3, "Math is broken") // Compilation error
Compile-Time String Processing
import scala.quoted.*
// Macro for compile-time string validation and processing
inline def validateEmail(inline email: String): String =
${ validateEmailImpl('email) }
def validateEmailImpl(email: Expr[String])(using Quotes): Expr[String] = {
email.value match {
case Some(emailStr) =>
if (emailStr.contains("@") && emailStr.contains(".")) {
email
} else {
quotes.reflect.report.errorAndAbort(s"Invalid email format: $emailStr")
}
case None =>
quotes.reflect.report.errorAndAbort("Email must be a string literal")
}
}
// Usage
val validEmail = validateEmail("user@example.com") // OK
// val invalidEmail = validateEmail("invalid-email") // Compilation error
// Compile-time string interpolation with validation
inline def sql(inline query: String): PreparedStatement =
${ sqlImpl('query) }
def sqlImpl(query: Expr[String])(using Quotes): Expr[PreparedStatement] = {
query.value match {
case Some(queryStr) =>
// Validate SQL syntax at compile time
if (queryStr.toLowerCase.startsWith("select") ||
queryStr.toLowerCase.startsWith("insert") ||
queryStr.toLowerCase.startsWith("update") ||
queryStr.toLowerCase.startsWith("delete")) {
'{ PreparedStatement(${query}) }
} else {
quotes.reflect.report.errorAndAbort(s"Invalid SQL query: $queryStr")
}
case None =>
quotes.reflect.report.errorAndAbort("SQL query must be a string literal")
}
}
case class PreparedStatement(query: String)
// Usage
val selectQuery = sql("SELECT * FROM users WHERE id = ?")
// val invalidQuery = sql("DROP TABLE users") // Compilation error
Advanced Macro Techniques
AST Manipulation and Code Generation
import scala.quoted.*
// Macro that generates getter methods for case class fields
inline def generateGetters[T]: String = ${ generateGettersImpl[T] }
def generateGettersImpl[T: Type](using Quotes): Expr[String] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val typeSymbol = tpe.typeSymbol
if (!typeSymbol.flags.is(Flags.Case)) {
report.errorAndAbort(s"${typeSymbol.name} is not a case class")
}
val fields = typeSymbol.caseFields
val getterMethods = fields.map { field =>
val fieldName = field.name
val fieldType = tpe.memberType(field).show
s"def get${fieldName.capitalize}: $fieldType = this.$fieldName"
}
val result = getterMethods.mkString("\n")
Expr(result)
}
// Usage with case class
case class Person(name: String, age: Int, email: String)
val getters = generateGetters[Person]
println(getters)
// Output:
// def getName: String = this.name
// def getAge: Int = this.age
// def getEmail: String = this.email
// Macro that creates a builder pattern for case classes
inline def generateBuilder[T]: String = ${ generateBuilderImpl[T] }
def generateBuilderImpl[T: Type](using Quotes): Expr[String] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val typeSymbol = tpe.typeSymbol
val className = typeSymbol.name
if (!typeSymbol.flags.is(Flags.Case)) {
report.errorAndAbort(s"$className is not a case class")
}
val fields = typeSymbol.caseFields
val builderClass = s"${className}Builder"
val fieldDeclarations = fields.map { field =>
val fieldName = field.name
val fieldType = tpe.memberType(field).show
s"private var _$fieldName: Option[$fieldType] = None"
}.mkString("\n ")
val setterMethods = fields.map { field =>
val fieldName = field.name
val fieldType = tpe.memberType(field).show
s"""def with${fieldName.capitalize}(value: $fieldType): $builderClass = {
| _$fieldName = Some(value)
| this
| }""".stripMargin
}.mkString("\n\n ")
val buildMethod = {
val requiredFields = fields.map { field =>
val fieldName = field.name
s"_$fieldName.getOrElse(throw new IllegalStateException(\"$fieldName is required\"))"
}.mkString(", ")
s"""def build(): $className = {
| $className($requiredFields)
| }""".stripMargin
}
val builderCode = s"""class $builderClass {
| $fieldDeclarations
|
| $setterMethods
|
| $buildMethod
|}
|
|object $builderClass {
| def apply(): $builderClass = new $builderClass()
|}""".stripMargin
Expr(builderCode)
}
// Usage
case class User(id: Long, name: String, email: String, age: Int)
val builderCode = generateBuilder[User]
println(builderCode)
Type-Level Programming with Macros
import scala.quoted.*
// Macro for compile-time type checking and transformation
trait TypeInfo[T] {
def typeName: String
def isNumeric: Boolean
def isPrimitive: Boolean
}
object TypeInfo {
inline given derived[T]: TypeInfo[T] = ${ deriveTypeInfo[T] }
}
def deriveTypeInfo[T: Type](using Quotes): Expr[TypeInfo[T]] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val typeName = tpe.show
val isNumeric = tpe <:< TypeRepr.of[Byte] ||
tpe <:< TypeRepr.of[Short] ||
tpe <:< TypeRepr.of[Int] ||
tpe <:< TypeRepr.of[Long] ||
tpe <:< TypeRepr.of[Float] ||
tpe <:< TypeRepr.of[Double] ||
tpe <:< TypeRepr.of[BigInt] ||
tpe <:< TypeRepr.of[BigDecimal]
val isPrimitive = tpe.typeSymbol.flags.is(Flags.Final) &&
(tpe <:< TypeRepr.of[AnyVal] || tpe =:= TypeRepr.of[String])
'{
new TypeInfo[T] {
def typeName: String = ${Expr(typeName)}
def isNumeric: Boolean = ${Expr(isNumeric)}
def isPrimitive: Boolean = ${Expr(isPrimitive)}
}
}
}
// Usage
def printTypeInfo[T](value: T)(using typeInfo: TypeInfo[T]): Unit = {
println(s"Value: $value")
println(s"Type: ${typeInfo.typeName}")
println(s"Is numeric: ${typeInfo.isNumeric}")
println(s"Is primitive: ${typeInfo.isPrimitive}")
}
// These will have compile-time generated TypeInfo instances
printTypeInfo(42)
printTypeInfo("Hello")
printTypeInfo(3.14)
printTypeInfo(List(1, 2, 3))
Macro-Generated DSL
import scala.quoted.*
// DSL for building SQL queries at compile time
case class SqlQuery(sql: String, params: List[Any])
// Macro for type-safe SQL DSL
inline def query(inline builder: QueryBuilder ?=> SqlQuery): SqlQuery =
${ queryImpl('builder) }
def queryImpl(builder: Expr[QueryBuilder ?=> SqlQuery])(using Quotes): Expr[SqlQuery] = {
import quotes.reflect.*
// This is a simplified implementation
// In practice, you'd analyze the AST to validate SQL syntax
'{
given QueryBuilder = new QueryBuilder()
$builder
}
}
class QueryBuilder {
private var tableName: String = ""
private var columns: List[String] = List("*")
private var whereConditions: List[String] = Nil
private var parameters: List[Any] = Nil
def select(cols: String*): QueryBuilder = {
columns = cols.toList
this
}
def from(table: String): QueryBuilder = {
tableName = table
this
}
def where(condition: String, params: Any*): QueryBuilder = {
whereConditions = whereConditions :+ condition
parameters = parameters ++ params
this
}
def build(): SqlQuery = {
val sql = s"SELECT ${columns.mkString(", ")} FROM $tableName" +
(if (whereConditions.nonEmpty) s" WHERE ${whereConditions.mkString(" AND ")}" else "")
SqlQuery(sql, parameters)
}
}
// Extension methods for QueryBuilder
extension (qb: QueryBuilder) {
def result: SqlQuery = qb.build()
}
// Usage
val userQuery = query {
val qb = summon[QueryBuilder]
qb.select("id", "name", "email")
.from("users")
.where("age > ?", 18)
.where("status = ?", "active")
.result
}
println(userQuery.sql)
// Output: SELECT id, name, email FROM users WHERE age > ? AND status = ?
println(userQuery.params)
// Output: List(18, active)
Compile-Time Code Generation
Automatic Serialization/Deserialization
import scala.quoted.*
import scala.deriving.Mirror
// Macro for generating JSON serializers
trait JsonEncoder[T] {
def encode(value: T): String
}
trait JsonDecoder[T] {
def decode(json: String): Option[T]
}
object JsonCodec {
inline def derived[T](using m: Mirror.Of[T]): JsonCodec[T] =
${ deriveJsonCodec[T]('m) }
}
trait JsonCodec[T] extends JsonEncoder[T] with JsonDecoder[T]
def deriveJsonCodec[T: Type](mirror: Expr[Mirror.Of[T]])(using Quotes): Expr[JsonCodec[T]] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val typeSymbol = tpe.typeSymbol
if (typeSymbol.flags.is(Flags.Case)) {
deriveCaseClassCodec[T](mirror)
} else {
report.errorAndAbort(s"Cannot derive JsonCodec for non-case class ${typeSymbol.name}")
}
}
def deriveCaseClassCodec[T: Type](mirror: Expr[Mirror.Of[T]])(using Quotes): Expr[JsonCodec[T]] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val typeSymbol = tpe.typeSymbol
val fields = typeSymbol.caseFields
// Generate encoder
val encodeImpl = {
val fieldEncodings = fields.map { field =>
val fieldName = field.name
s""""$fieldName": " + encodeValue(value.$fieldName) + """"
}.mkString(""" + "," + """)
s"""def encode(value: ${tpe.show}): String = {
| "{" + $fieldEncodings + "}"
|}""".stripMargin
}
// Generate decoder (simplified)
val decodeImpl = s"""def decode(json: String): Option[${tpe.show}] = {
| // Simplified JSON parsing - in practice use a proper JSON library
| try {
| Some(parseJson(json))
| } catch {
| case _: Exception => None
| }
|}""".stripMargin
'{
new JsonCodec[T] {
def encode(value: T): String = {
def encodeValue(v: Any): String = v match {
case s: String => "\"" + s + "\""
case n: Int => n.toString
case n: Long => n.toString
case n: Double => n.toString
case b: Boolean => b.toString
case other => "\"" + other.toString + "\""
}
// This would be generated per case class
value match {
case _ => "{}" // Simplified implementation
}
}
def decode(json: String): Option[T] = {
// Simplified implementation
None
}
}
}
}
// Usage
case class Person(name: String, age: Int, email: String)
given JsonCodec[Person] = JsonCodec.derived[Person]
val person = Person("Alice", 30, "alice@example.com")
val json = summon[JsonCodec[Person]].encode(person)
Performance Optimization Macros
import scala.quoted.*
// Macro for unrolling loops at compile time
inline def unroll[T](inline n: Int)(inline body: Int => T): Unit =
${ unrollImpl('n, 'body) }
def unrollImpl[T: Type](n: Expr[Int], body: Expr[Int => T])(using Quotes): Expr[Unit] = {
n.value match {
case Some(nValue) if nValue <= 20 => // Limit unrolling to prevent code bloat
val statements = (0 until nValue).map { i =>
'{ ${body}(${Expr(i)}) }
}
Expr.block(statements.toList, '{ () })
case Some(nValue) =>
quotes.reflect.report.warning(s"Loop unrolling skipped for n=$nValue (too large)")
'{
for (i <- 0 until $n) {
$body(i)
}
}
case None =>
quotes.reflect.report.errorAndAbort("Loop count must be a compile-time constant")
}
}
// Usage
unroll(5) { i =>
println(s"Iteration $i")
}
// This generates:
// println(s"Iteration 0")
// println(s"Iteration 1")
// println(s"Iteration 2")
// println(s"Iteration 3")
// println(s"Iteration 4")
// Compile-time memoization
inline def memoize[A, B](inline f: A => B): A => B =
${ memoizeImpl('f) }
def memoizeImpl[A: Type, B: Type](f: Expr[A => B])(using Quotes): Expr[A => B] = {
'{
val cache = scala.collection.mutable.Map.empty[A, B]
(a: A) => cache.getOrElseUpdate(a, $f(a))
}
}
// Usage
val fibonacci: Int => Long = memoize { n: Int =>
if (n <= 1) n.toLong
else fibonacci(n - 1) + fibonacci(n - 2)
}
Error Handling and Diagnostics
Custom Error Messages
import scala.quoted.*
// Macro with detailed error reporting
inline def requirePositive[T](inline value: T)(using num: Numeric[T]): T =
${ requirePositiveImpl('value) }
def requirePositiveImpl[T: Type](value: Expr[T])(using Quotes): Expr[T] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
if (!(tpe <:< TypeRepr.of[Numeric[_]])) {
report.errorAndAbort(s"requirePositive can only be used with numeric types, got ${tpe.show}")
}
'{
val v = $value
if (summon[Numeric[T]].compare(v, summon[Numeric[T]].zero) <= 0) {
throw new IllegalArgumentException(s"Expected positive value, got $v")
}
v
}
}
// Macro with position-aware error reporting
inline def todo(inline message: String): Nothing =
${ todoImpl('message) }
def todoImpl(message: Expr[String])(using Quotes): Expr[Nothing] = {
import quotes.reflect.*
val pos = Position.ofMacroExpansion
val fileName = pos.sourceFile.name
val line = pos.startLine + 1
val column = pos.startColumn + 1
val msg = message.valueOrAbort
report.errorAndAbort(s"TODO: $msg (at $fileName:$line:$column)")
}
// Usage
def calculateSomething(x: Int): Int = {
val positive = requirePositive(x)
todo("Implement the actual calculation") // Compilation error with location
}
Macro Testing and Debugging
import scala.quoted.*
// Macro for inspecting AST at compile time
inline def inspectAst[T](inline expr: T): T = ${ inspectAstImpl('expr) }
def inspectAstImpl[T: Type](expr: Expr[T])(using Quotes): Expr[T] = {
import quotes.reflect.*
val term = expr.asTerm
val pretty = Printer.TreeAnsiCode.show(term)
val structure = Printer.TreeStructure.show(term)
println(s"=== AST Inspection ===")
println(s"Expression: ${term.show}")
println(s"Type: ${TypeRepr.of[T].show}")
println(s"Pretty Print:\n$pretty")
println(s"Structure:\n$structure")
println(s"====================")
expr
}
// Usage (will print AST information at compile time)
val result = inspectAst(List(1, 2, 3).map(_ * 2).filter(_ > 2))
// Macro for compile-time benchmarking
inline def compileBenchmark[T](inline name: String)(inline code: T): T =
${ compileBenchmarkImpl('name, 'code) }
def compileBenchmarkImpl[T: Type](name: Expr[String], code: Expr[T])(using Quotes): Expr[T] = {
val startTime = System.nanoTime()
val result = code
val endTime = System.nanoTime()
val duration = (endTime - startTime) / 1_000_000.0
val nameStr = name.valueOrAbort
println(s"Compile-time benchmark '$nameStr': ${duration}ms")
result
}
Best Practices and Guidelines
Macro Design Principles
// 1. Keep macros simple and focused
// Bad: One macro doing everything
inline def megaMacro[T](inline value: T): String = ${ megaMacroImpl('value) }
// Good: Separate concerns
inline def debug[T](inline value: T): T = ${ debugImpl('value) }
inline def typeInfo[T]: String = ${ typeInfoImpl[T] }
// 2. Provide good error messages
def validateImpl[T: Type](value: Expr[T])(using Quotes): Expr[T] = {
import quotes.reflect.*
// Bad: Generic error
// report.errorAndAbort("Validation failed")
// Good: Specific, actionable error
val pos = Position.ofMacroExpansion
val tpe = TypeRepr.of[T].show
report.errorAndAbort(
s"Validation failed for type $tpe at ${pos.sourceFile.name}:${pos.startLine + 1}. " +
s"Expected a case class with at least one field."
)
}
// 3. Handle edge cases gracefully
def safeFieldAccess[T: Type](fieldName: Expr[String])(using Quotes): Expr[Option[Any]] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val fieldNameStr = fieldName.valueOrAbort
val field = tpe.typeSymbol.memberFields.find(_.name == fieldNameStr)
field match {
case Some(f) =>
// Generate safe field access code
'{ Some(???) }
case None =>
report.warning(s"Field '$fieldNameStr' not found in type ${tpe.show}")
'{ None }
}
}
// 4. Use types to enforce correctness
trait Validated[T]
object Validated {
inline def apply[T](inline value: T): Validated[T] = ${ validateAndWrap[T]('value) }
}
def validateAndWrap[T: Type](value: Expr[T])(using Quotes): Expr[Validated[T]] = {
// Perform compile-time validation
// Return wrapped value only if validation passes
'{
new Validated[T] {
// Implementation
}
}
}
Performance Considerations
// Macro performance best practices
// 1. Cache expensive computations
object MacroCache {
private val typeInfoCache = scala.collection.mutable.Map.empty[String, String]
def getTypeInfo[T: Type](using Quotes): String = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val key = tpe.show
typeInfoCache.getOrElseUpdate(key, {
// Expensive computation here
generateTypeInfo(tpe)
})
}
private def generateTypeInfo(tpe: quotes.reflect.TypeRepr): String = {
// Implementation
tpe.show
}
}
// 2. Minimize AST traversal
def efficientAnalysis[T: Type](using Quotes): Expr[String] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
// Good: Single traversal with accumulated results
val (fieldCount, methodCount, isCase) = tpe.typeSymbol match {
case sym =>
val fields = sym.memberFields
val methods = sym.memberMethods
val isCase = sym.flags.is(Flags.Case)
(fields.length, methods.length, isCase)
}
Expr(s"Fields: $fieldCount, Methods: $methodCount, Case class: $isCase")
}
// 3. Use lazy evaluation for optional features
class MacroAnalyzer[T: Type](using Quotes) {
import quotes.reflect.*
private val tpe = TypeRepr.of[T]
lazy val fields = tpe.typeSymbol.memberFields
lazy val methods = tpe.typeSymbol.memberMethods
lazy val companions = tpe.typeSymbol.companionModule
def generateFieldInfo(): Expr[String] = {
// Only computes fields when actually needed
Expr(fields.map(_.name).mkString(", "))
}
}
Real-World Examples
Configuration DSL
// Macro-powered configuration DSL
import scala.quoted.*
case class Config(values: Map[String, Any])
inline def config(inline block: ConfigBuilder ?=> Unit): Config =
${ configImpl('block) }
def configImpl(block: Expr[ConfigBuilder ?=> Unit])(using Quotes): Expr[Config] = {
'{
val builder = new ConfigBuilder()
given ConfigBuilder = builder
$block
builder.build()
}
}
class ConfigBuilder {
private val values = scala.collection.mutable.Map.empty[String, Any]
def set(key: String, value: Any): Unit = {
values(key) = value
}
def build(): Config = Config(values.toMap)
}
extension (key: String) {
def :=(value: Any)(using builder: ConfigBuilder): Unit = {
builder.set(key, value)
}
}
// Usage
val appConfig = config {
"database.url" := "jdbc:postgresql://localhost/mydb"
"database.username" := "user"
"database.password" := "secret"
"server.port" := 8080
"server.host" := "0.0.0.0"
}
Type-Safe Query Builder
// Advanced type-safe query builder with macros
import scala.quoted.*
trait Table[T] {
def tableName: String
def columns: List[String]
}
class Query[T](val sql: String, val parameters: List[Any])
inline def table[T]: Table[T] = ${ deriveTable[T] }
def deriveTable[T: Type](using Quotes): Expr[Table[T]] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T]
val symbol = tpe.typeSymbol
val tableName = symbol.name.toLowerCase
val columns = symbol.caseFields.map(_.name)
'{
new Table[T] {
def tableName: String = ${Expr(tableName)}
def columns: List[String] = ${Expr(columns)}
}
}
}
inline def select[T](columns: String*): QueryBuilder[T] =
${ selectImpl[T]('columns) }
def selectImpl[T: Type](columns: Expr[Seq[String]])(using Quotes): Expr[QueryBuilder[T]] = {
'{
new QueryBuilder[T](${columns}.toList)
}
}
class QueryBuilder[T](selectedColumns: List[String]) {
def from(table: Table[T]): FromClause[T] = {
new FromClause[T](selectedColumns, table)
}
}
class FromClause[T](columns: List[String], table: Table[T]) {
def where(condition: String, params: Any*): WhereClause[T] = {
new WhereClause[T](columns, table, List(condition), params.toList)
}
def build(): Query[T] = {
val columnStr = if (columns.isEmpty) "*" else columns.mkString(", ")
val sql = s"SELECT $columnStr FROM ${table.tableName}"
new Query[T](sql, Nil)
}
}
class WhereClause[T](
columns: List[String],
table: Table[T],
conditions: List[String],
params: List[Any]
) {
def and(condition: String, newParams: Any*): WhereClause[T] = {
new WhereClause[T](columns, table, conditions :+ condition, params ++ newParams)
}
def build(): Query[T] = {
val columnStr = if (columns.isEmpty) "*" else columns.mkString(", ")
val whereStr = if (conditions.nonEmpty) s" WHERE ${conditions.mkString(" AND ")}" else ""
val sql = s"SELECT $columnStr FROM ${table.tableName}$whereStr"
new Query[T](sql, params)
}
}
// Usage
case class User(id: Long, name: String, email: String, age: Int)
given Table[User] = table[User]
val query = select[User]("name", "email")
.from(summon[Table[User]])
.where("age > ?", 18)
.and("email LIKE ?", "%@example.com")
.build()
println(query.sql)
// Output: SELECT name, email FROM user WHERE age > ? AND email LIKE ?
Conclusion
Scala 3 macros provide powerful metaprogramming capabilities for:
Code Generation:
- Automatic derivation of type class instances
- Builder pattern generation
- Serialization/deserialization code
- Performance optimizations through unrolling
Compile-Time Safety:
- Static validation and assertions
- Type-safe DSLs with compile-time checking
- Configuration validation
- SQL query verification
Developer Experience:
- Better error messages with precise locations
- IDE integration with full type information
- Debugging tools for macro development
- Performance analysis at compile time
Best Practices:
- Keep macros focused and composable
- Provide clear error messages
- Cache expensive computations
- Use types to enforce correctness
Scala 3 macros enable powerful compile-time programming while maintaining type safety and IDE support, making them an excellent tool for library authors and application developers who need advanced code generation and validation capabilities.
Comments
Be the first to comment on this lesson!