Search code examples
javascalareflectionintrospection

Access a value's parent naming from within the instantiated class (Scala)?


Assume Scala 2.11. I'm writing a class that will persist a Scala value. It's intention is to be used as such:

class ParentClass {

  val instanceId: String = "aUniqueId"

  val statefulString: Persisted[String] = persisted { "SomeState" }

  onEvent {
    case NewState(state) => statefulString.update(state)
  }

}

Persisted is a class with a type parameter that is meant to persist that specific value like a cache, and Persist handles all of the logic associated with persistence. However, to simply the implementation, I'm hoping to retrieve information about it's instantiation. For example, if it's instance in the parent class is named statefulString, how can I access that name from within the Persisted class itself?

The purpose of doing this is to prevent collisions in automatic naming of persisted values while simplifying the API. I cannot rely on using type, because there could be multiple values of String type.

Thanks for your help!

Edit

This question may be helpful: How can I get the memory location of a object in java?

Edit 2

After reading the source code for ScalaCache, it appears there is a way to do this via WeakTypeTag. Can someone explain what exactly is happening in its macros?

https://github.com/cb372/scalacache/blob/960e6f7aef52239b85fa0a1815a855ab46356ad1/core/src/main/scala/scalacache/memoization/Macros.scala


Solution

  • I was able to do this with the help of Scala macros and reflection, and adapting some code from ScalaCache:

    class Macros(val c: blackbox.Context) {
      import c.universe._
    
      def persistImpl[A: c.WeakTypeTag, Repr: c.WeakTypeTag](f: c.Tree)(keyPrefix: c.Expr[ActorIdentifier], scalaCache: c.Expr[ScalaCache[Repr]], flags: c.Expr[Flags], ec: c.Expr[ExecutionContext], codec: c.Expr[Codec[A, Repr]]) = {
        commonMacroImpl(keyPrefix,  scalaCache, { keyName =>
          q"""_root_.persistence.sync.caching($keyName)($f)($scalaCache, $flags, $ec, $codec)"""
        })
      }
    
      private def commonMacroImpl[A: c.WeakTypeTag, Repr: c.WeakTypeTag](keyPrefix: c.Expr[ActorIdentifier], scalaCache: c.Expr[ScalaCache[Repr]], keyNameToCachingCall: (c.TermName) => c.Tree): Tree = {
    
        val enclosingMethodSymbol = getMethodSymbol()
        val valNameTree = getValName(enclosingMethodSymbol)
    
        val keyName = createKeyName()
        val scalacacheCall = keyNameToCachingCall(keyName)
        val tree = q"""
              val $keyName = _root_.persistence.KeyStringConverter.createKeyString($keyPrefix, $valNameTree)
              $scalacacheCall
            """
        tree
      }
    
      /**
        * Get the symbol of the method that encloses the macro,
        * or abort the compilation if we can't find one.
        */
      private def getValSymbol(): c.Symbol = {
    
        def getValSymbolRecursively(sym: Symbol): Symbol = {
          if (sym == null || sym == NoSymbol || sym.owner == sym)
            c.abort(
              c.enclosingPosition,
              "This persistence block does not appear to be inside a val. " +
                "Memoize blocks must be placed inside vals, so that a cache key can be generated."
            )
          else if (sym.isTerm)
            try {
              val termSym = sym.asInstanceOf[TermSymbol]
              if(termSym.isVal) termSym
              else getValSymbolRecursively(sym.owner)
            } catch {
              case NonFatal(e) => getValSymbolRecursively(sym.owner)
            }
          else
            getValSymbolRecursively(sym.owner)
        }
    
        getValSymbolRecursively(c.internal.enclosingOwner)
      }
    
      /**
        * Convert the given method symbol to a tree representing the method name.
        */
      private def getValName(methodSymbol: c.Symbol): c.Tree = {
        val methodName = methodSymbol.asMethod.name.toString
        // return a Tree
        q"$methodName"
      }
    
      private def createKeyName(): TermName = {
        // We must create a fresh name for any vals that we define, to ensure we don't clash with any user-defined terms.
        // See https://github.com/cb372/scalacache/issues/13
        // (Note that c.freshName("key") does not work as expected.
        // It causes quasiquotes to generate crazy code, resulting in a MatchError.)
        c.freshName(c.universe.TermName("key"))
      }
    
    }