2

Is there a way to get the parent class from an instance of an inner class using macros rather than run-time reflection?

I have a set of classes like this:

trait IdProvider {
  type IdObject = Id.type
  case class Id(underlying: Int)
}

case class SomeEntity(id: SomeEntity.Id)

object SomeEntity extends IdProvider

And some code that works with arbitrary IdProvider#Ids:

val lookup = Map[IdProvider#IdObject, Set[Operation]]

def can(operation: Operation, id: IdProvider#Id): Boolean = {
    val idObject = findIdTypeFromInstance(id)  // This is what I don't have
    lookup.get(idObject).exists(s => s(operation))
}

Taking a leaf out of this gist by Paul P. I now have this macro:

def findIdTypeFromInstance[T <: AnyRef : c.WeakTypeTag](
    c: blackbox.Context)(thing: c.Expr[T]): c.Expr[T] = {
  import c.universe._
  val companion = thing.actualType.typeSymbol.companion match {
    case NoSymbol =>
      c.abort(c.enclosingPosition, s"Instance of ${thing.actualType} has no companion object")
    case sym => sym
  }

  def make[U: c.WeakTypeTag] = c.Expr[U](internal.gen.mkAttributedRef(companion))

  make(c.WeakTypeTag(companion.typeSignature))
}

This works for simpler cases (top level case classes, classes and objects, and even nested case classes). However, when dealing with the IdProvider setup above the macro tries to generate this tree:

Select(This(TypeName("IdProvider")), TermName("Id"))

This results in an extremely long stack trace in my test, which starts with:

scala.reflect.internal.Types$TypeError: value is not a member of my.spec.MacroSpec

I have not been able to find a path from the instance or the companion (IdProvider#Id) to the parent class (in this case SomeEntity). Is there a way to get to SomeEntity or do I have to use run-time reflection?

Community
  • 1
  • 1
Sean Vieira
  • 155,703
  • 32
  • 311
  • 293

1 Answers1

1

The Id companion is basically a lazy val. You need the enclosing instance to retrieve its value because it's not a statically defined stable path.

With -Yshow-syms you can see it get added in mixin phase:

      object SomeEntity
          constructor SomeEntity
*         method Id$lzycompute (private)
          method apply (case <synthetic>)
              value id
          method readResolve (private <synthetic>)
          method unapply (case <synthetic>)
              value x$0 (<synthetic>)
*         object Id (<synthetic> <stable>)
          value <local SomeEntity>
*         variable Id$module (private <local> <synthetic>)

The $outer field of an Id is added in explicitouter.

Is it easier just to expose the companion reference explicitly?

  case class Id(underlying: Int) {
    def c = Id
  }

This is just a quick look; maybe there's a clever way to do it.

som-snytt
  • 39,429
  • 2
  • 47
  • 129
  • That's a good idea - I'm already exposing the outer instance (via a `self` type) so I may be able to get in that way. – Sean Vieira Oct 22 '15 at 18:46