It is known in scala 2 that macros are strictly local and are only executed once, when the class is defined. This feature seems particularly weak when combining with abstract type, as the process to convert abstract types into concrete one generally bypasses macro and uses its own primitive rules.

One simple example that demonstrates a counter-intuitive outcome is in the following test code:

  trait BB {

    def ttag = implicitly[TypeTag[this.type]]
  }

  case class AA() extends BB

  it("can TypeTag") {

    val kk = AA()

    TypeViz(kk.ttag).peek // this function visualise the full type tree of the type tag
  }

if executed, the type of kk turns out to be:

-+ BB.this.type
 !-+ InfoCTSpec.this.BB
   !-+ Object
     !-- Any

Oops, the type AA is completely ignored, because implicitly[TypeTag[this.type]] is backed by a built-in macro implicit, which is only executed ONCE when BB is defined, not when AA is defined and reify the actual kk.this.type. I find it quite unwieldy and prone to cause some other features (e.g. pattern matching, type lambda) to degrade due to runtime type erasure.

I'd like to write/use a language extension to, e.g. make TypeTag[this.type] a subtype of AA, WITHOUT introducing runtime overhead & out-of-scope context objects (so, NO IMPLICIT). How can I do this with the least amount of hacking? I'm open to very hardcore solutions like compiler extension and macro, but an elegant work-around that can be smoothly carried over to scala 3/dotty is obviously preferred.

P.S. it appears that the dotty "inline/compiletime" feature has partially implemented what I have envisioned. Is this the correct impression?

1

There are 1 answers

0
Dmytro Mitin On

You are free to write macros and compiler plugins but the conventional mechanism to postpone implicit resolution from definition site to call site is to replace implicitly with an implicit parameter

trait BB {
  def ttag(implicit tt: TypeTag[this.type]) = tt
}

When doing implicit resolution with type parameters, why does val placement matter?

WITHOUT introducing runtime overhead & out-of-scope context objects (so, NO IMPLICIT).

It's not clear why you want to avoid implicits. Implicits are resolved at compile time. If you replace this with macros or compiler plugins then anyway you'll kind of resolve implicits manually at compile time.


Alternatively you can postpone implicit resolution with a macro (this is like inlining in Scala 3)

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

trait BB {
  def ttag: Any = macro Macros.ttagImpl
}

object Macros {
  def ttagImpl(c: whitebox.Context): c.Tree = {
    import c.universe._
    q"_root_.scala.Predef.implicitly[_root_.scala.reflect.runtime.universe.TypeTag[this.type]]"
  }
}

or

import scala.reflect.runtime.{universe => ru}

object Macros {
  def ttagImpl(c: whitebox.Context): c.Tree = {
    import c.universe._
    c.inferImplicitValue(appliedType(typeOf[ru.TypeTag[_]].typeConstructor, internal.thisType(c.internal.enclosingOwner.owner/*c.macroApplication.symbol*/)), silent = false)
  }
}

Implicit Json Formatter for value classes in Scala