Get an implicit value during macro generation for a Type

61 Views Asked by At

Macro generates an empty instance of a case class based on implicitly provided values for all the properties.

The current state of the solution can be found on GH: https://github.com/atais/empty/blob/main/empty-macro/src/main/scala/com/github/atais/empty/EmptyMacro.scala

Currently, the definition of the macro expands to:

implicit val emptyFoo0: Empty[Foo0] = {
  ...
  override val value: com.github.atais.Foo0 = new com.github.atais.Foo0(implicitly[Empty[String]].value, implicitly[Empty[Boolean]].value, implicitly[Empty[Int]].value)
  ...
}

Which works fine, but I would like to inline the implicit values into the generated code. I believe it should be faster (will see about that). So I expect it to be:

implicit val emptyFoo0: Empty[Foo0] = {
  ...
  override def value: com.github.atais.Foo0 = new com.github.atais.Foo0("", false, 0)
  ...
}

So I believe the code that requires improvement is:

weakTypeOf[A].decls
  .collect {
    case m: MethodSymbol if m.isCaseAccessor => m
  }
  .map(_.returnType)
  .map { fieldType =>
    val emptyImplicitType = tq"Empty[$fieldType]"
    q"implicitly[$emptyImplicitType].value"
  }

ChatGPT helped me to the point:

  .map { fieldType =>
        val empty = tq"Empty[$fieldType]"
        val t = c.typecheck(empty, c.TYPEmode)
        val i = c.inferImplicitValue(t.tpe)
        val q = q"$i.value"
        q
  }

which gives

    override val value: com.github.atais.empty.Foo30 = new com.github.atais.empty.Foo30(MacroEmptyInstances.this.emptyFoo28.value, MacroEmptyInstances.this.emptyFoo29.value)

But I don't know how to pull the values into generated code.

1

There are 1 best solutions below

1
Dmytro Mitin On

Try to use c.eval. Replace

val q = q"$i.value"
q

with

val q = q"$i.value"
val qValue = c.eval(c.Expr[Any](c.untypecheck(q)))
qValue match {
  case v: Int     => q"$v"
  case v: Boolean => q"$v"
  case v: String  => q"$v"
  // ... other types from (*)
}

or

val iValue = c.eval(c.Expr[Empty[Any]](c.untypecheck(i)))
val qValue = iValue.value
qValue match {
  case v: Int     => q"$v"
  case v: Boolean => q"$v"
  case v: String  => q"$v"
  // ... other types from (*)
}

(*) https://docs.scala-lang.org/overviews/quasiquotes/lifting.html#standard-liftables

But you'll have to re-organize your project a little (implicit instances). Currently this fails with compile error java.lang.ClassNotFoundException: atais.empty.MacroEmptyInstances$.

The following code compiles:

macro project:

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

object Macros {
  class Empty[A](val value: A) extends AnyVal
  object Empty {
    def apply[A](x: A): Empty[A] = new Empty[A](x)

    implicit val stringEmpty: Empty[String] = Empty("")
    implicit val booleanEmpty: Empty[Boolean] = Empty(false)
    implicit val intEmpty: Empty[Int] = Empty(0)

    implicit def deriveEmpty[A]: Empty[A] = macro deriveEmptyImpl[A]

    def deriveEmptyImpl[A: c.WeakTypeTag](
                                           c: whitebox.Context
                                         ): c.Expr[Empty[A]] = {
      import c.universe._

      val aType = weakTypeOf[A]

      if (!aType.typeSymbol.asClass.isCaseClass) {
        c.abort(c.enclosingPosition, s"$aType is not a case class")
      }

      val values = aType.decls
        .collect {
          case m: MethodSymbol if m.isCaseAccessor => m
        }
        .map(_.returnType)
        .map { fieldType =>
          val tpe = appliedType(typeOf[Empty[_]].typeConstructor, fieldType)
          val i = c.inferImplicitValue(tpe)
          val iValue = c.eval(c.Expr[Empty[Any]](c.untypecheck(i)))
          val qValue = iValue.value
          qValue match {
            case v: Int     => q"$v"
            case v: Boolean => q"$v"
            case v: String  => q"$v"
            // ... other types from (*)
          }
        }

      c.Expr[Empty[A]] {
        q"""new Empty[$aType](new $aType(..$values))"""
      }
    }
  }
}

core project:

import Macros.Empty

object App {
  case class Foo(str: String, b: Boolean, i: Int)

  implicitly[Empty[Foo]] // scalacOptions += "-Ymacro-debug-lite"
  // scala: new Empty[App.Foo](new App.Foo("", false, 0))
}