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

Let's connect

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