1

I'm using the RunningStrategies class in my Scala code to define a set of running strategies. To create an instance of RunningStrategies, I'm using the apply method along with a variable number of arguments representing the different strategies. To achieve this, I have also defined a macro called RunningStrategiesMacros.

However, when I try to use RunningStrategies with some strategies, I get the following error message: "macro implementation not found: apply". The error seems to be related to the fact that I'm trying to use the macro implementation in the same compilation run that defines it.

RunningStrategy:

package com.dv.phoenix.brandsafety.models

object RunningStrategy extends Enumeration {
  type RunningStrategy = Value

  val
  MonitoringOnly,
  BlockingOnly,
  MonitoringAndBlockingInherent,
  MonitoringAndBlockingRecalculate
  = Value
}

RunningStrategies:

package com.dv.phoenix.brandsafety.models

import com.dv.phoenix.brandsafety.models.RunningStrategy.RunningStrategy
import com.dv.phoenix.brandsafety.utils.RunningStrategiesMacros

import scala.language.experimental.macros
case class RunningStrategies private (runningStrategies: Set[RunningStrategy])
object RunningStrategies {
  def apply(strategies: RunningStrategy*): RunningStrategies = macro RunningStrategiesMacros.applyImp
}

RunningStrategiesMacro:

package com.dv.phoenix.brandsafety.utils

import com.dv.phoenix.brandsafety.models.RunningStrategies
import com.dv.phoenix.brandsafety.models.RunningStrategy.RunningStrategy

import scala.reflect.macros.blackbox

object RunningStrategiesMacros {
   def applyImp(c: blackbox.Context)(runningStrategies: c.Expr[RunningStrategy]*): c.Expr[RunningStrategies] = {
    import c.universe._

    val hasMonitoringAndBlockingInherent = runningStrategies.map(_.tree.toString).contains(s"${RunningStrategy.getClass.getName.stripSuffix("$")}.${RunningStrategy.MonitoringAndBlockingInherent}")
    val hasMonitoringAndBlockingRecalculate = runningStrategies.map(_.tree.toString).contains(s"${RunningStrategy.getClass.getName.stripSuffix("$")}.${RunningStrategy.MonitoringAndBlockingRecalculate}")

    if (hasMonitoringAndBlockingInherent && hasMonitoringAndBlockingRecalculate) {
      c.abort(c.enclosingPosition, "runningStrategies cannot include both RunningStrategy.MonitoringAndBlockingInherent and RunningStrategy.MonitoringAndBlockingRecalculate")
    } else {
      c.Expr(q"RunningStrategies(Set(..$runningStrategies))")
    }
  }

}

Usuage:

override protected val runningStrategies: RunningStrategies = RunningStrategies(MonitoringOnly, MonitoringAndBlockingInherent, MonitoringAndBlockingRecalculate)

I got the following error:

macro implementation not found: apply
(the most common reason for that is that you cannot use macro implementations in the same compilation run that defines them)
  override protected val runningStrategies: RunningStrategies = RunningStrategies(MonitoringOnly, MonitoringAndBlockingInherent, MonitoringAndBlockingRecalculate)

I have noticed that the scala-logging library has managed to solve this problem with their LoggerImpl and LoggerMacro classes. Can someone explain how they were able to do this and how I can apply the same technique to my RunningStrategiesMacro?

Zvi Mints
  • 1,072
  • 1
  • 8
  • 20
  • 1
    It's difficult to say exactly (you haven't provided the code where you override `protected val runningStrategies`) but something like https://github.com/DmytroMitin/SO-Q75847326-macros-demo `RunningStrategies` and `RunningStrategiesMacros` depend on each other (because you use `c.Expr[...]` macro signatures rather than `c.Tree`), so they should go to the same subproject (`macros`). Where you override `runningStrategies` (my `App`, macro application) should be different from `macros`, so it goes to `core`. – Dmytro Mitin Mar 27 '23 at 08:32
  • Both `core` and `macros` depend on `RunningStrategy`, so `RunningStrategy` can go to `macros` or to `common`. – Dmytro Mitin Mar 27 '23 at 08:32
  • 1
    https://github.com/DmytroMitin/SO-Q75847326-macros-demo/commit/c6d9f1f69d1ff901ab852b93bab91c4597e72fe5 – Dmytro Mitin Mar 27 '23 at 08:39
  • Awesome! thanks!! (I'm still encountering a problem while trying to implement the `applyImp`. I need to abort the `RunningStrategies(MonitoringAndBlockingInherent, MonitoringAndBlockingRecalculate)` part. If you have a solution for this, that would be wonderful!) – Zvi Mints Mar 27 '23 at 15:17
  • Update: I managed to solve it, i updated the `applyImp` - probably there nicely way to do it – Zvi Mints Mar 27 '23 at 17:37
  • If you have more questions feel free to open a new question – Dmytro Mitin Mar 27 '23 at 20:23
  • All solved, cheers @DmytroMitin – Zvi Mints Mar 27 '23 at 20:27
  • 1
    Work on raw strings doesn't look good (especially those `.stripSuffix("$")`). It's better to compare by `Symbol` https://github.com/DmytroMitin/SO-Q75847326-macros-demo/commit/81fc7dd4491805dcc799864242eea0ed02abe4ce See https://docs.scala-lang.org/overviews/reflection/symbols-trees-types.html – Dmytro Mitin Mar 27 '23 at 20:57
  • 1
    One more option is to use `c.eval` https://github.com/DmytroMitin/SO-Q75847326-macros-demo/commit/e5878860c6fd0945fd7782d23eb2de61e67e63ad – Dmytro Mitin Mar 27 '23 at 21:06
  • Thanks! I used it and its looks much better – Zvi Mints Mar 27 '23 at 21:39

1 Answers1

3

In your question you described your package structure but not your project structure. Packages and subprojects are different concepts

Why can't sbt/assembly find the main classes in a project with multiple subprojects?

You should have different subprojects if you want to use macros in Scala 2

https://www.scala-sbt.org/1.x/docs/Macro-Projects.html

Firstly, let's refresh the terminology. This is a macro definition

import scala.language.experimental.macros

def foo(): Unit = macro fooImpl

This is a macro implementation

import scala.reflect.macros.blackbox

def fooImpl(c: blackbox.Context)(): c.Tree = {
  import c.universe._
  println("test")
  q"""_root_.scala.Predef.println("foo")"""
}

This is a macro application (call site, macro expansion)

foo()
// at compile time: scalac: test
// at runtime: foo

In Scala 2 macro implementations must be compiled before macro applications (the compile time of macro applications i.e. macro expansion is the runtime of macros). So it's macro implementations and macro applications that must be in different compile units. For example in different subprojects of your project. Or in src/main/scala and src/test/scala of the same project (main code is compiled before tests). Where macro definitions are is not so important. They can be in the same compilation unit/subproject as macro implementations (maybe in the same file/class) or in the same as macro applications or in their own.

In scala-logging, macro definitions

def error(message: String): Unit = macro LoggerMacro.errorMessage

and macro implementations

def errorMessage(c: LoggerContext)(message: c.Expr[String]): c.universe.Tree = ...

are in the same project (in different files but this is not necessary, they can be in the same file) but macro applications

logger.error(msg)

are in src/test/scala https://github.com/lightbend-labs/scala-logging/blob/v3.9.5/src/test/scala/com/typesafe/scalalogging/LoggerSpec.scala . And other macro applications will be also in different compilation units i.e. client projects using scala-logging as a dependency.

In your project

def apply(strategies: RunningStrategy*): RunningStrategies = macro RunningStrategiesMacros.applyImp

is a macro definition and

def applyImp(c: blackbox.Context)(strategies: c.Expr[RunningStrategy]*): c.Expr[RunningStrategies] = ...

is a macro implementation. But you should re-organize your project having at least two subprojects and place macro applications

override protected val runningStrategies: RunningStrategies = RunningStrategies.apply(MonitoringOnly, MonitoringAndBlockingInherent, MonitoringAndBlockingRecalculate)

to a different subproject

lazy val commonSettings = Seq(
  scalaVersion := "2.13.10"
)

// where you override runningStrategies goes here
lazy val core = project
  .dependsOn(macros)
  .settings(
    commonSettings,
    scalacOptions += "-Ymacro-debug-lite" // convenient to debug macro expansions
  )

// utils/RunningStrategiesMacros go here
lazy val macros = project
  .settings(
    commonSettings,
    // necessary for macro implementations
    libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
  )

Is there any trick to use macros in the same file they are defined?

Possible to identify/use Scala macros using reflection or similar?

Using scala macro from java

Implicit materialization in the same module

Def macro - scala 2.13 - not found: value cond

Scala macros and separate compilation units

Auto-Generate Companion Object for Case Class in Scala

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66