TL;DR
- inline: Inlines code at compile time to eliminate runtime overhead
- inline if/match: Conditional compilation, type-specific optimization
- compiletime package: Compile-time operations like
error,constValue,summonInline- Macros (
${ }): Compile-time code generation usingscala.quotedAPI- Scala 3 macros are more type-safe and concise than Scala 2
Target Audience: Advanced developers familiar with type-level programming Prerequisites: Generics, type classes, advanced type system
Metaprogramming enables code generation and validation at compile time. Scala 3 provides inline and a new macro system. By leveraging these features, you can reduce boilerplate code, perform optimizations at compile time, and enable type-safe code generation.
📚 Prerequisites: This is an advanced topic. You should be familiar with:
- Generics - Type parameters
- Type Classes - Type-level abstraction
- Advanced Types - Match Types, Type Lambdas
Difficulty: ⭐⭐⭐⭐⭐ (Very Advanced)
Inline#
The inline keyword inlines code at compile time. Inlined functions are replaced with their function body at the call site, eliminating runtime overhead.
Basic Usage
// Method inlining
inline def twice(x: Int): Int = x + x
val result = twice(21) // Replaced with 42 at compile timeConstant Folding
Using inline val inlines constants at compile time. This allows constant operations to be computed at compile time, improving runtime performance.
inline val Pi = 3.14159
// Computed at compile time
inline def circleArea(radius: Double): Double =
Pi * radius * radius
val area = circleArea(5) // Compiled to 78.53975Conditional Compilation
Using inline if selects code at compile time based on conditions. Branches with false conditions are completely removed from the compiled bytecode.
// First, define Config object
object Config:
inline val Debug = true // or false
inline def debug(inline msg: String): Unit =
inline if Config.Debug then
println(msg)
else
() // Removed at compile time
debug("Debug message") // If Config.Debug is false, the code itself is removedKey Points
inline def: Replaced with function body at call siteinline val: Constants inlined at compile timeinline if: Code selected at compile time based on condition
Inline Match#
Performs pattern matching at compile time. inline match selects the appropriate branch at compile time based on input type, eliminating runtime pattern matching overhead.
inline def toInt(x: Any): Int = inline x match
case x: Int => x
case x: String => x.toInt
case x: Double => x.toInt
toInt(42) // Int branch selected at compile time
toInt("42") // String branch selected at compile timeType-Specific Optimization
transparent inline enables more precise return type inference. Optimized code is generated for each type, eliminating unnecessary conversions or boxing.
transparent inline def stringify[T](x: T): String =
inline x match
case x: Int => x.toString
case x: String => x
case x: Double => f"$x%.2f"
case _ => x.toString
val s1: String = stringify(42) // "42"
val s2: String = stringify("hello") // "hello"
val s3: String = stringify(3.14159) // "3.14"Key Points
inline match: Compile-time pattern matchingtransparent inline: More precise return type inference- Type-specific optimized code generation
Compile-time Operations#
Scala 3 provides various features for compile-time operations. The scala.compiletime package includes utility functions that are evaluated at compile time.
compiletime Package
Functions like error, constValue, summonInline enable you to raise errors or extract values at compile time.
import scala.compiletime.*
// Compile-time error
inline def checkPositive(inline n: Int): Int =
inline if n <= 0 then
error("n must be positive")
else
n
checkPositive(5) // OK
// checkPositive(-1) // Compile error: n must be positiveconstValue
constValue retrieves the value of a literal type at compile time. Useful for bringing type-level constants to the value level.
import scala.compiletime.constValue
// Extract value from literal type
inline def literalValue[T <: Int]: Int = constValue[T]
val three = literalValue[3] // Replaced with 3 at compile time
// Practical example: Tuple size
import scala.compiletime.ops.int.*
type TupleSize[T <: Tuple] = T match
case EmptyTuple => 0
case h *: t => 1 + TupleSize[t]summonInline
summonInline summons a type class instance at compile time. If the instance doesn’t exist, it causes a compile error.
import scala.compiletime.summonInline
trait Show[A]:
def show(a: A): String
inline def show[A](a: A): String =
summonInline[Show[A]].show(a)Key Points
error: Raise compile-time errorsconstValue: Extract values from literal typessummonInline: Summon type class instances at compile time
Macros#
Scala 3 macros use the quotes API. Macros enable code analysis and generation at compile time. More powerful than inline but also more complex.
Simple Macro
Macros are defined using ${ ... } splicing and '{ ... } quoting. Splicing executes at compile time, while quoting represents runtime code.
import scala.quoted.*
// Macro definition
inline def printCode(inline x: Any): Unit = ${ printCodeImpl('x) }
def printCodeImpl(x: Expr[Any])(using Quotes): Expr[Unit] =
import quotes.reflect.*
'{ println(${Expr(x.show)}) }
// Usage
printCode(1 + 2) // Prints "1 + 2"Expression Generation
The Expr type allows creating and manipulating expressions at compile time, enabling type-safe code generation.
import scala.quoted.*
inline def toStringMacro[T](x: T): String = ${ toStringImpl('x) }
def toStringImpl[T: Type](x: Expr[T])(using Quotes): Expr[String] =
'{ ${x}.toString }Key Points
${ ... }: Splicing, executes at compile time'{ ... }: Quoting, represents runtime codeExpr[T]: Type-safe expression wrapper
Scala 2 vs Scala 3 Macros#
The macro systems in Scala 2 and Scala 3 are completely different. Scala 3 significantly improved type safety, but migrating Scala 2 macros requires complete rewriting. The table below summarizes the key differences between the two versions.
| Feature | Scala 2 | Scala 3 |
|---|---|---|
| API | scala.reflect.macros | scala.quoted |
| Safety | Low | High (Staged) |
| Complexity | High | Relatively low |
| Migration | - | Complete rewrite required |
Practical Use Cases#
Macros and inline are used for various practical purposes. Common examples include automatic logging, extracting type names, and compile-time validation.
1. Automatic Logging
Using inline, you can implement automatic logging for debugging without overhead.
inline def logged[T](inline block: T): T =
val result = block
println(s"Result: $result")
result
val x = logged {
val a = 1
val b = 2
a + b
} // Prints "Result: 3"2. Type Name Extraction
Combining Mirror and constValue allows extracting type names at compile time.
import scala.compiletime.constValue
import scala.deriving.Mirror
inline def typeName[T](using m: Mirror.Of[T]): String =
constValue[m.MirroredLabel]
case class Person(name: String, age: Int)
typeName[Person] // "Person"3. Compile-time Validation
Using the error function allows raising compile errors under specific conditions, catching incorrect usage at compile time rather than runtime.
import scala.compiletime.error
inline def requirePositive(inline n: Int): Int =
inline if n <= 0 then
error("Value must be positive")
else
n
val valid = requirePositive(5) // OK
// val invalid = requirePositive(-1) // Compile errorBest Practices#
Guidelines for effectively using macros and inline.
DO
The following situations recommend using inline and macros:
- Use
inlinefor performance-critical small functions - Use macros for compile-time validation
- Use macros for boilerplate code generation
DON’T
These are anti-patterns to avoid:
- Don’t make every function
inline(increases compile time) - Don’t implement complex logic with macros
- Avoid macro overuse that makes debugging difficult
Exercises#
Practice inline and macro concepts with the following exercises.
1. Compile-time Computation ⭐⭐⭐
Write an inline function that computes Fibonacci numbers at compile time.
View Answer
inline def fib(inline n: Int): Int =
inline if n <= 1 then n
else fib(n - 1) + fib(n - 2)
val f10 = fib(10) // Replaced with 55 at compile timeReferences#
For more details, refer to the official documentation.
Next Steps#
- Concurrency — Future, Promise
- Functional Patterns — Functor, Monad