The motivation is to offload computations away from runtime when possible. If one has any knowledge about the execution of the code, presumably based on configuration or client code that won’t change, it would be beneficial to evaluate as much as possible during compile time.
Macros allow these evaluations. The result is called “expended code” that will be inlined at the place of the call to the macro after compilation.
I’ve used it for tracing (events going out to datadog):
trace("Request received via HTTP interface") // this method accepts two implicit parameters (traceContext, tracedService)
This trace
is a macro method. The implementation of the method is:
def trace(message: String)(implicit context: TraceContext, tracedService: TracedService): Unit = macro traceImpl
This is called def macro
. traceImpl
is the definition itself:
private val TracedServiceTag = "serviceName"
private val Title = "Traffic-Tracing"
val TraceId = "traceId"
val CompanyId = "companyId"
def traceImpl(c: blackbox.Context)(message: c.Expr[String])
(context: c.Expr[TraceContext], tracedService: c.Expr[TracedService]): c.universe.Tree = {
import c.universe._
q"""
if (TrafficTracing.isTraceEnabled($context)) {
import kamon.Kamon
val serviceNameT = $TracedServiceTag -> $tracedService
val title = $Title + " | " + serviceNameT._2
Kamon.event(
title,
$message,
Map(
$TraceId -> $context($TraceId),
$CompanyId -> $context($CompanyId),
serviceNameT
)
)
}
"""
}
Here we can see the usage of blackbox.Context
which is a must. This is the compilation context which is injected into the method, from which we derrive all our subsequent information.
When writing macros, we deal with the underlying AST which represents the values and state that exists at the time of compilation. Because the syntax of handling the ASTs is not friendly, the quasiquotes
function is used to transform normal code syntax to AST syntax (q"""<code to be transformed to AST syntax>"""
).
- The macro definition has to be written in a static context.
- The macro first and only (curried) variable has to be the
(c: blackbox.Context)
. - The macro body has to import
c.universe._
. - The macro def cannot be invoked in the same module in which it was wrote.
- I couldn't make string interpolation work inside quasiquotes.
- This is an experimental feature, so things don't always work as expected.
if (TrafficTracing.isTraceEnabled)
will be executed inline. This is a small optimization, but this check will run for each request, so we may argue that it may be insignificant per invocation, but over great quantity of invocations, the improvement may be noticable.
Awesome !!