Skip to content

Instantly share code, notes, and snippets.

@sankemax
Last active September 29, 2020 08:03
Show Gist options
  • Save sankemax/7f8982899bf41190bc876806d370c28f to your computer and use it in GitHub Desktop.
Save sankemax/7f8982899bf41190bc876806d370c28f to your computer and use it in GitHub Desktop.

Macros are a feature that was introduced in Scala 2.10:

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>""").

There are rules to writing a macro:

  1. The macro definition has to be written in a static context.
  2. The macro first and only (curried) variable has to be the (c: blackbox.Context).
  3. The macro body has to import c.universe._.
  4. The macro def cannot be invoked in the same module in which it was wrote.

Some personal guidelines and findings:

  1. I couldn't make string interpolation work inside quasiquotes.
  2. This is an experimental feature, so things don't always work as expected.

So what is the benefit of this macro over a non-macro solution?

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.

@elig-salt
Copy link

Awesome !!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment