Inline and macros

Scala · Expert · 20 min

Libraries use these for zero-overhead abstractions, derivation and compile-time checks.

Scala 3 has a principled metaprogramming system built in layers.

`inline` is the first layer. An inline def is guaranteed to be inlined at each call site. Combined with inline parameters, inline if and inline match, the branches are resolved during compilation, so code specialised to constants costs nothing at runtime. transparent inline methods may even return a more specific type than declared.

scala.compiletime adds tools usable inside inline code: error("msg") emits a custom compile error, constValue[T] turns a literal type into a value, erasedValue[T] lets you match on types, and summonInline/summonFrom look up givens at expansion time. This is enough for a lot of type-class derivation with scala.deriving.Mirror.

Macros are the second layer. An inline method calls ${ impl('x) }, and impl is a regular method running in the compiler that receives typed code as Expr[T] and returns new code built with quotes '{ ... } and splices ${ ... }. Macros are type-checked, hygienic and, unlike Scala 2 macros, part of the stable language. The reflection API (quotes.reflect) gives full access to trees when quotes aren't enough.

Rules: a macro implementation must be defined in a different file (compiled earlier) than its call sites, and macros can't introduce new definitions visible to the user's code. Use inline first and reach for macros only when you need to inspect or generate code.

Example

import scala.compiletime.{error, constValue, erasedValue}

inline def power(x: Double, inline n: Int): Double =
  inline if n == 0 then 1.0
  else inline if n % 2 == 1 then x * power(x, n - 1)
  else power(x * x, n / 2)                    // unrolled at compile time

inline def checkedPort(inline p: Int): Int =
  inline if p < 1 || p > 65535 then error("port out of range") else p

inline def typeName[T]: String = inline erasedValue[T] match
  case _: Int    => "Int"
  case _: String => "String"
  case _         => "other"
type Size = 3
@main def meta(): Unit =
  println(power(2.0, 10))                     // 1024.0
  println(checkedPort(8080))                  // 8080
  // checkedPort(70000)                       // compile error: port out of range
  println(typeName[String])                   // String
  println(constValue[Size] * 2)               // 6
// Macro sketch (impl must live in another file):
// inline def show(inline x: Any): String = ${ showImpl('x) }
// def showImpl(x: Expr[Any])(using Quotes): Expr[String] = Expr(x.show)

Remember

  • inline guarantees inlining; inline if/match resolve at compile time
  • scala.compiletime gives error, constValue and erasedValue
  • Macros use quotes '{} and splices ${} over typed Expr trees
  • Macro implementations must be compiled before their call sites

Watch out for

  • Reaching for macros when an inline method with compiletime operations would do.

Check yourself

  1. What happens when checkedPort(70000) is compiled with the definition above?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/scala3/reference/metaprogramming/inline.html

Builds on

Unlocks

Nothing builds on this yet.