Let's connect
Let's connect

Metaprogramming in Scala 2 & 3

Picture of Jan Chyb, Scala 3 and Tooling Specialist

Jan Chyb

Scala 3 and Tooling Specialist

15 minutes read

scala

// macros.scala
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

import example.util.NonZeroNumber

object Macros { 
  def nonZeroNum(number: Int): NonZeroNumber = macro nonZeroNum_impl

  def nonZeroNum_impl(
      c: Context
    )(number: c.Expr[Int]) = {
    import c.universe._

    // unpack a static value from the method argument
    val Literal(Constant(constValue: Int)) = number.tree

    // construct a NonZeroNumber based on that value
    if (constValue > 0) {
      q"_root_.example.util.Positive($number)"
    } else if (constValue < 0) {
      q"_root_.example.util.Negative($number)"
    } else {
      c.abort(c.enclosingPosition, "Non zero value expected")
    }
  }
}

scala

// example/util/NonZeroNumber.scala
package example.util
sealed trait NonZeroNumber
case class Positive(value: Int) extends NonZeroNumber
case class Negative(value: Int) extends NonZeroNumber

scala

// main.scala
object Main {
  def main(args: Array[String]): Unit = {
    println(Macros.nonZeroNum(5)) // Positive(5)
    println(Macros.nonZeroNum(15)) // Positive(15)
    println(Macros.nonZeroNum(-5)) // Negative(-5)
    // println(Macros.nonZeroNum(0)) // will cause a compile time error

scala

inline def method() = …

scala

inline def method(inline value: Int) = …

scala

inline def nonZeroNum(inline value: Int): NonZeroNumber = {
  inline if (value > 0) {
    Positive(value)
  } else inline if (value < 0) {
    Negative(value)
  } else {
    scala.compiletime.error("Non zero value expected")
  }
}

scala

transparent inline def nonZeroNum(inline value: Int): NonZeroNumber = {
  inline if (value > 0) {
    Positive(value)
  } else inline if (value < 0) {
    Negative(value)
  } else {
    scala.compiletime.error("Non zero value expected")
  }
}

scala

// macros.scala
import example.util._
import scala.quoted._

object Macros {
  transparent inline def nonZeroNum(inline value: Int): NonZeroNumber =
    ${ Macros.nonZeroNumImpl('value) }

  def nonZeroNumImpl(using Quotes)(value: Expr[Int]): Expr[NonZeroNumber] = {
    // unpack a static value from the method argument
    val constValue = value.valueOrAbort

    // construct a NonZeroNumber based on that value
    if (constValue > 0) {
      '{ Positive($value) }
    } else if (constValue < 0) {
      '{ Negative($value) }
    } else {
      quotes.reflect.report.errorAndAbort("Non zero value expected")
    }
  }
}

scala

import scala.quoted._

object Macros {
  transparent inline def cnf(
      inline cnfSpec: String, inline booleans: Boolean*
    ): Boolean =
    ${ cnfImpl('cnfSpec, 'booleans) }

  def cnfImpl(using Quotes)(
      cnfSpec: Expr[String], booleanSeqExpr: Expr[Seq[Boolean]]
    ): Expr[Boolean] = {

    // "unpack" Expr[Seq[T]] to Seq[Expr[T]]
    val Varargs(booleans: Seq[Expr[Boolean]]) = booleanSeqExpr

    // parse cnf string
    case class Literal(negated: Boolean, refNum: Int)
    val cnfStr = cnfSpec.valueOrAbort
    val clauseStrs = cnfStr.split('^')
    val clauses: Array[Array[Literal]] = clauseStrs
      .map(num => num.substring(1, num.size - 1))
      .map(_.split('v').map(literalStr =>
        if (literalStr.startsWith("-")) Literal(negated=true, literalStr.substring(1).toInt)
        else Literal(negated=false, literalStr.toInt)
      ))

    // finally, map the data to code via quoting and splicing
    clauses.map { clause =>
      clause.foldLeft('{false}){ (acc: Expr[Boolean], literal: Literal) =>
        if (literal.negated) '{ $acc || !${booleans(literal.refNum)} } // Expr[Boolean]
        else '{ $acc || ${booleans(literal.refNum)} } // Expr[Boolean]
      }
    }.foldLeft('{true}){ (acc: Expr[Boolean], booleanClause: Expr[Boolean]) =>
      '{ $acc && $booleanClause } // Expr[Boolean]
    }
  }
}

scala

def example(c: Context) = {
  import c.universe._
  val customAst: tree = {
    // create a custom AST tree
  }

  q"""
    code
    ${customAst}
    code
  """
}

scala

def example(using Quotes) = {
  '{
    code
    ${customAst()}
    code
  }
}

def customAst(using Quotes) = { // different Quotes instance than in the example method 
  import quotes.reflect._
  // create a custom program tree
}

Liked the article?

Share it with others!

explore more on

Take the first step to a sustained competitive edge for your business

Get your free consultation

VirtusLab's work has met the mark several times over, and their latest project is no exception. The team is efficient, hard-working, and trustworthy. Customers can expect a proactive team that drives results.

Stephen Rooke
Stephen RookeDirector of Software Development @ Extreme Reach

VirtusLab's engineers are truly Strapi extensions experts. Their knowledge and expertise in the area of Strapi plugins gave us the opportunity to lift our multi-brand CMS implementation to a different level.

facile logo
Leonardo PoddaEngineering Manager @ Facile.it

VirtusLab has been an incredible partner since the early development of Scala 3, essential to a mature and stable Scala 3 ecosystem.

Martin_Odersky
Martin OderskyHead of Programming Research Group @ EPFL

The VirtusLab team's in-depth knowledge, understanding, and experience of technology have been invaluable to us in developing our product. The team is professional and delivers on time – we greatly appreciated this efficiency when working with them.

Michael_Grant
Michael GrantDirector of Development @ Cyber Sec Company